Introduce code from Rust monorepo. This is a mess

This commit is contained in:
Brandon Thomas
2022-02-28 05:43:54 -05:00
parent 921fb49f9a
commit ba326bfcfd
1058 changed files with 192192 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
NeMo (**Ne**ural **Mo**dules) is a toolkit for creating AI applications built around **neural modules**, conceptual blocks of neural networks that take *typed* inputs and produce *typed* outputs.
**NeMo Core** provides common APIs all modules and models have to implement.
**NeMo Collections**
* ASR - collection of modules and models for building speech recognition networks
* TTS - collection of modules and models for building speech synthesis networks
* NLP - collection of modules and models for building NLP networks
+28
View File
@@ -0,0 +1,28 @@
# Copyright (c) 2020, 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.
from nemo.package_info import (
__contact_emails__,
__contact_names__,
__description__,
__download_url__,
__homepage__,
__keywords__,
__license__,
__package_name__,
__repository_url__,
__shortversion__,
__version__,
)
+13
View File
@@ -0,0 +1,13 @@
# Copyright (c) 2020, 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.
+25
View File
@@ -0,0 +1,25 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr import data, losses, models, modules
from nemo.package_info import __version__
# Set collection version equal to NeMo version.
__version = __version__
# Authorship.
__author__ = "NVIDIA Corporation"
# Set collection name.
__description__ = "Automatic Speech Recognition collection"
+13
View File
@@ -0,0 +1,13 @@
# Copyright (c) 2020, 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.
@@ -0,0 +1,921 @@
# Copyright (c) 2020, 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 io
import math
import os
from typing import Dict, List, Optional, Union
import braceexpand
import torch
import webdataset as wd
from nemo.collections.asr.parts.preprocessing.segment import available_formats as valid_sf_formats
from nemo.collections.common.parts.preprocessing import collections
from nemo.core.classes import Dataset, IterableDataset
from nemo.core.neural_types import AudioSignal, LabelsType, LengthsType, NeuralType, RegressionValuesType
from nemo.utils import logging
# List of valid file formats (prioritized by order of importance)
VALID_FILE_FORMATS = ';'.join(['wav', 'mp3', 'flac'] + [fmt.lower() for fmt in valid_sf_formats.keys()])
def repeat_signal(signal, sig_len, required_length):
"""repeat signal to make short signal to have required_length
Args:
signal (FloatTensor): input signal
sig_len (LongTensor): length of input signal
required_length(float) : length of generated signal
Returns:
signal (FloatTensor): generated signal of required_length by repeating itself.
"""
repeat = required_length // sig_len
rem = required_length % sig_len
sub = signal[-rem:] if rem > 0 else torch.tensor([])
rep_sig = torch.cat(repeat * [signal])
signal = torch.cat((rep_sig, sub))
return signal
def normalize(signal):
"""normalize signal
Args:
signal(FloatTensor): signal to be normalized.
"""
signal_minusmean = signal - signal.mean()
return signal_minusmean / signal_minusmean.abs().max()
def count_occurence(manifest_file_id):
"""Count number of wav files in Dict manifest_file_id. Use for _TarredAudioToLabelDataset.
Args:
manifest_file_id (Dict): Dict of files and their corresponding id. {'A-sub0' : 1, ..., 'S-sub10':100}
Returns:
count (Dict): Dict of wav files {'A' : 2, ..., 'S':10}
"""
count = dict()
for i in manifest_file_id:
audio_filename = i.split("-sub")[0]
count[audio_filename] = count.get(audio_filename, 0) + 1
return count
def _speech_collate_fn(batch, pad_id):
"""collate batch of audio sig, audio len, tokens, tokens len
Args:
batch (Optional[FloatTensor], Optional[LongTensor], LongTensor,
LongTensor): A tuple of tuples of signal, signal lengths,
encoded tokens, and encoded tokens length. This collate func
assumes the signals are 1d torch tensors (i.e. mono audio).
"""
_, audio_lengths, _, tokens_lengths = zip(*batch)
max_audio_len = 0
has_audio = audio_lengths[0] is not None
if has_audio:
max_audio_len = max(audio_lengths).item()
max_tokens_len = max(tokens_lengths).item()
audio_signal, tokens = [], []
for sig, sig_len, tokens_i, tokens_i_len in batch:
if has_audio:
sig_len = sig_len.item()
if sig_len < max_audio_len:
pad = (0, max_audio_len - sig_len)
sig = torch.nn.functional.pad(sig, pad)
audio_signal.append(sig)
tokens_i_len = tokens_i_len.item()
if tokens_i_len < max_tokens_len:
pad = (0, max_tokens_len - tokens_i_len)
tokens_i = torch.nn.functional.pad(tokens_i, pad, value=pad_id)
tokens.append(tokens_i)
if has_audio:
audio_signal = torch.stack(audio_signal)
audio_lengths = torch.stack(audio_lengths)
else:
audio_signal, audio_lengths = None, None
tokens = torch.stack(tokens)
tokens_lengths = torch.stack(tokens_lengths)
return audio_signal, audio_lengths, tokens, tokens_lengths
def _fixed_seq_collate_fn(self, batch):
"""collate batch of audio sig, audio len, tokens, tokens len
Args:
batch (Optional[FloatTensor], Optional[LongTensor], LongTensor,
LongTensor): A tuple of tuples of signal, signal lengths,
encoded tokens, and encoded tokens length. This collate func
assumes the signals are 1d torch tensors (i.e. mono audio).
"""
fixed_length = self.featurizer.sample_rate * self.time_length
_, audio_lengths, _, tokens_lengths = zip(*batch)
has_audio = audio_lengths[0] is not None
fixed_length = int(min(fixed_length, max(audio_lengths)))
audio_signal, tokens, new_audio_lengths = [], [], []
for sig, sig_len, tokens_i, _ in batch:
if has_audio:
sig_len = sig_len.item()
chunck_len = sig_len - fixed_length
if chunck_len < 0:
repeat = fixed_length // sig_len
rem = fixed_length % sig_len
sub = sig[-rem:] if rem > 0 else torch.tensor([])
rep_sig = torch.cat(repeat * [sig])
signal = torch.cat((rep_sig, sub))
new_audio_lengths.append(torch.tensor(fixed_length))
else:
start_idx = torch.randint(0, chunck_len, (1,)) if chunck_len else torch.tensor(0)
end_idx = start_idx + fixed_length
signal = sig[start_idx:end_idx]
new_audio_lengths.append(torch.tensor(fixed_length))
audio_signal.append(signal)
tokens.append(tokens_i)
if has_audio:
audio_signal = torch.stack(audio_signal)
audio_lengths = torch.stack(new_audio_lengths)
else:
audio_signal, audio_lengths = None, None
tokens = torch.stack(tokens)
tokens_lengths = torch.stack(tokens_lengths)
return audio_signal, audio_lengths, tokens, tokens_lengths
def _sliced_seq_collate_fn(self, batch):
"""collate batch of audio sig, audio len, tokens, tokens len
Args:
batch (Optional[FloatTensor], Optional[LongTensor], LongTensor,
LongTensor): A tuple of tuples of signal, signal lengths,
encoded tokens, and encoded tokens length. This collate func
assumes the signals are 1d torch tensors (i.e. mono audio).
"""
slice_length = self.featurizer.sample_rate * self.time_length
_, audio_lengths, _, tokens_lengths = zip(*batch)
shift = self.shift_length * self.featurizer.sample_rate
has_audio = audio_lengths[0] is not None
audio_signal, num_slices, tokens, audio_lengths = [], [], [], []
for sig, sig_len, tokens_i, _ in batch:
if has_audio:
sig_len = sig_len.item()
dur = sig_len / self.featurizer.sample_rate
base = math.ceil((dur - self.time_length) / self.shift_length)
slice_length = int(slice_length)
shift = int(shift)
slices = 1 if base < 0 else base + 1
for slice_id in range(slices):
start_idx = slice_id * shift
end_idx = start_idx + slice_length
signal = sig[start_idx:end_idx]
if len(signal) < slice_length:
signal = repeat_signal(signal, len(signal), slice_length)
audio_signal.append(signal)
num_slices.append(slices)
tokens.extend([tokens_i] * slices)
audio_lengths.extend([slice_length] * slices)
if has_audio:
audio_signal = torch.stack(audio_signal)
audio_lengths = torch.tensor(audio_lengths)
else:
audio_signal, audio_lengths = None, None
tokens = torch.stack(tokens)
tokens_lengths = torch.tensor(num_slices) # each embedding length
return audio_signal, audio_lengths, tokens, tokens_lengths
def _vad_frame_seq_collate_fn(self, batch):
"""collate batch of audio sig, audio len, tokens, tokens len
Args:
batch (Optional[FloatTensor], Optional[LongTensor], LongTensor,
LongTensor): A tuple of tuples of signal, signal lengths,
encoded tokens, and encoded tokens length. This collate func
assumes the signals are 1d torch tensors (i.e. mono audio).
batch size equals to 1.
"""
slice_length = int(self.featurizer.sample_rate * self.time_length)
_, audio_lengths, _, tokens_lengths = zip(*batch)
slice_length = min(slice_length, max(audio_lengths))
shift = int(self.featurizer.sample_rate * self.shift_length)
has_audio = audio_lengths[0] is not None
audio_signal, num_slices, tokens, audio_lengths = [], [], [], []
append_len_start = slice_length // 2
append_len_end = slice_length - slice_length // 2
for sig, sig_len, tokens_i, _ in batch:
if self.normalize_audio:
sig = normalize(sig)
start = torch.zeros(append_len_start)
end = torch.zeros(append_len_end)
sig = torch.cat((start, sig, end))
sig_len += slice_length
if has_audio:
slices = (sig_len - slice_length) // shift
for slice_id in range(slices):
start_idx = slice_id * shift
end_idx = start_idx + slice_length
signal = sig[start_idx:end_idx]
audio_signal.append(signal)
num_slices.append(slices)
tokens.extend([tokens_i] * slices)
audio_lengths.extend([slice_length] * slices)
if has_audio:
audio_signal = torch.stack(audio_signal)
audio_lengths = torch.tensor(audio_lengths)
else:
audio_signal, audio_lengths = None, None
tokens = torch.stack(tokens)
tokens_lengths = torch.tensor(num_slices)
return audio_signal, audio_lengths, tokens, tokens_lengths
class _AudioLabelDataset(Dataset):
"""
Dataset that loads tensors via a json file containing paths to audio files,
labels, and durations and offsets(in seconds). Each new line is a
different sample. Example below:
and their target labels. JSON files should be of the following format::
{"audio_filepath": "/path/to/audio_wav_0.wav", "duration": time_in_sec_0, "label": \
target_label_0, "offset": offset_in_sec_0}
...
{"audio_filepath": "/path/to/audio_wav_n.wav", "duration": time_in_sec_n, "label": \
target_label_n, "offset": offset_in_sec_n}
Args:
manifest_filepath (str): Dataset parameter. Path to JSON containing data.
labels (list): Dataset parameter. List of target classes that can be output by the speaker recognition model.
featurizer
min_duration (float): Dataset parameter. All training files which have a duration less than min_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to 0.1.
max_duration (float): Dataset parameter.
All training files which have a duration more than max_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to None.
trim (bool): Whether to use trim silence from beginning and end of audio signal using librosa.effects.trim().
Defaults to False.
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
output_types = {
'audio_signal': NeuralType(
('B', 'T'),
AudioSignal(freq=self._sample_rate)
if self is not None and hasattr(self, '_sample_rate')
else AudioSignal(),
),
'a_sig_length': NeuralType(tuple('B'), LengthsType()),
}
if self.is_regression_task:
output_types.update(
{
'targets': NeuralType(tuple('B'), RegressionValuesType()),
'targets_length': NeuralType(tuple('B'), LengthsType()),
}
)
else:
output_types.update(
{'label': NeuralType(tuple('B'), LabelsType()), 'label_length': NeuralType(tuple('B'), LengthsType()),}
)
return output_types
def __init__(
self,
*,
manifest_filepath: str,
labels: List[str],
featurizer,
min_duration: Optional[float] = 0.1,
max_duration: Optional[float] = None,
trim: bool = False,
is_regression_task: bool = False,
):
super().__init__()
self.collection = collections.ASRSpeechLabel(
manifests_files=manifest_filepath.split(','),
min_duration=min_duration,
max_duration=max_duration,
is_regression_task=is_regression_task,
)
self.featurizer = featurizer
self.trim = trim
self.is_regression_task = is_regression_task
if not is_regression_task:
self.labels = labels if labels else self.collection.uniq_labels
self.num_classes = len(self.labels) if self.labels is not None else 1
self.label2id, self.id2label = {}, {}
for label_id, label in enumerate(self.labels):
self.label2id[label] = label_id
self.id2label[label_id] = label
for idx in range(len(self.labels[:5])):
logging.debug(" label id {} and its mapped label {}".format(idx, self.id2label[idx]))
else:
self.labels = []
self.num_classes = 1
def __len__(self):
return len(self.collection)
def __getitem__(self, index):
sample = self.collection[index]
offset = sample.offset
if offset is None:
offset = 0
features = self.featurizer.process(sample.audio_file, offset=offset, duration=sample.duration, trim=self.trim)
f, fl = features, torch.tensor(features.shape[0]).long()
if not self.is_regression_task:
t = torch.tensor(self.label2id[sample.label]).long()
else:
t = torch.tensor(sample.label).float()
tl = torch.tensor(1).long() # For compatibility with collate_fn used later
return f, fl, t, tl
# Ported from https://github.com/NVIDIA/OpenSeq2Seq/blob/master/open_seq2seq/data/speech2text/speech_commands.py
class AudioToClassificationLabelDataset(_AudioLabelDataset):
"""
Dataset that loads tensors via a json file containing paths to audio
files, command class, and durations (in seconds). Each new line is a
different sample. Example below:
{"audio_filepath": "/path/to/audio_wav_0.wav", "duration": time_in_sec_0, "label": \
target_label_0, "offset": offset_in_sec_0}
...
{"audio_filepath": "/path/to/audio_wav_n.wav", "duration": time_in_sec_n, "label": \
target_label_n, "offset": offset_in_sec_n}
Args:
manifest_filepath: Path to manifest json as described above. Can
be comma-separated paths.
labels (Optional[list]): String containing all the possible labels to map to
if None then automatically picks from ASRSpeechLabel collection.
featurizer: Initialized featurizer class that converts paths of
audio to feature tensors
max_duration: If audio exceeds this length, do not include in dataset
min_duration: If audio is less than this length, do not include
in dataset
trim: Boolean flag whether to trim the audio
"""
# self.labels = labels if labels else self.collection.uniq_labels
# self.num_commands = len(self.labels)
def _collate_fn(self, batch):
return _speech_collate_fn(batch, pad_id=0)
class AudioToSpeechLabelDataset(_AudioLabelDataset):
"""
Dataset that loads tensors via a json file containing paths to audio
files, command class, and durations (in seconds). Each new line is a
different sample. Example below:
{"audio_filepath": "/path/to/audio_wav_0.wav", "duration": time_in_sec_0, "label": \
target_label_0, "offset": offset_in_sec_0}
...
{"audio_filepath": "/path/to/audio_wav_n.wav", "duration": time_in_sec_n, "label": \
target_label_n, "offset": offset_in_sec_n}
Args:
manifest_filepath (str): Path to manifest json as described above. Can
be comma-separated paths.
labels (Optional[list]): String containing all the possible labels to map to
if None then automatically picks from ASRSpeechLabel collection.
min_duration (float): Dataset parameter.
All training files which have a duration less than min_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to 0.1.
max_duration (float): Dataset parameter.
All training files which have a duration more than max_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to None.
trim (bool): Whether to use trim silence from beginning and end
of audio signal using librosa.effects.trim().
Defaults to False.
time_length (float): time length of slice (in seconds)
Use this for speaker recognition and VAD tasks.
shift_length (float): amount of shift of window for generating the frame for VAD task in a batch
Use this for VAD task during inference.
normalize_audio (bool): Whether to normalize audio signal.
Defaults to False.
is_regression_task (bool): Whether the dataset is for a regression task instead of classification
"""
def __init__(
self,
*,
manifest_filepath: str,
labels: List[str],
featurizer,
min_duration: Optional[float] = 0.1,
max_duration: Optional[float] = None,
trim: bool = False,
time_length: Optional[float] = 8,
shift_length: Optional[float] = 1,
normalize_audio: bool = False,
is_regression_task: bool = False,
):
logging.info("Time length considered for collate func is {}".format(time_length))
logging.info("Shift length considered for collate func is {}".format(shift_length))
self.time_length = time_length
self.shift_length = shift_length
self.normalize_audio = normalize_audio
super().__init__(
manifest_filepath=manifest_filepath,
labels=labels,
featurizer=featurizer,
min_duration=min_duration,
max_duration=max_duration,
trim=trim,
is_regression_task=is_regression_task,
)
def fixed_seq_collate_fn(self, batch):
return _fixed_seq_collate_fn(self, batch)
def sliced_seq_collate_fn(self, batch):
return _sliced_seq_collate_fn(self, batch)
def vad_frame_seq_collate_fn(self, batch):
return _vad_frame_seq_collate_fn(self, batch)
class _TarredAudioLabelDataset(IterableDataset):
"""
A similar Dataset to the AudioLabelDataSet, but which loads tarred audio files.
Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToSpeechLabelDataset),
as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should
contain the information for one audio file, including at least the label and name of the audio
file within the tarball.
Valid formats for the audio_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or
(2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...].
Note: For brace expansion in (1), there may be cases where `{x..y}` syntax cannot be used due to shell interference.
This occurs most commonly inside SLURM scripts. Therefore we provide a few equivalent replacements.
Supported opening braces - { <=> (, [, < and the special tag _OP_.
Supported closing braces - } <=> ), ], > and the special tag _CL_.
For SLURM based tasks, we suggest the use of the special tags for ease of use.
See the documentation for more information about accepted data and input formats.
If using multiple processes the number of shards should be divisible by the number of workers to ensure an
even split among workers. If it is not divisible, logging will give a warning but training will proceed.
In addition, if using mutiprocessing, each shard MUST HAVE THE SAME NUMBER OF ENTRIES after filtering
is applied. We currently do not check for this, but your program may hang if the shards are uneven!
Notice that a few arguments are different from the AudioLabelDataSet; for example, shuffle (bool) has been
replaced by shuffle_n (int).
Additionally, please note that the len() of this DataLayer is assumed to be the length of the manifest
after filtering. An incorrect manifest length may lead to some DataLoader issues down the line.
Args:
audio_tar_filepaths: Either a list of audio tarball filepaths, or a
string (can be brace-expandable).
manifest_filepath (str): Path to the manifest.
labels (list): Dataset parameter.
List of target classes that can be output by the speaker recognition model.
featurizer
shuffle_n (int): How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
Defaults to 0.
min_duration (float): Dataset parameter.
All training files which have a duration less than min_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to 0.1.
max_duration (float): Dataset parameter.
All training files which have a duration more than max_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to None.
trim(bool): Whether to use trim silence from beginning and end
of audio signal using librosa.effects.trim().
Defaults to False.
time_length (float): time length of slice (in seconds) # Pass this only for speaker recognition and VAD task
shift_length (float): amount of shift of window for generating the frame for VAD task. in a batch # Pass this only for VAD task during inference.
normalize_audio (bool): Whether to normalize audio signal. Defaults to False.
shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp.
- `scatter`: The default shard strategy applied by WebDataset, where each node gets
a unique set of shards, which are permanently pre-allocated and never changed at runtime.
- `replicate`: Optional shard strategy, where each node gets all of the set of shards
available in the tarred dataset, which are permanently pre-allocated and never changed at runtime.
The benefit of replication is that it allows each node to sample data points from the entire
dataset independently of other nodes, and reduces dependence on value of `shuffle_n`.
Note: Replicated strategy allows every node to sample the entire set of available tarfiles,
and therefore more than one node may sample the same tarfile, and even sample the same
data points! As such, there is no assured guarantee that all samples in the dataset will be
sampled at least once during 1 epoch.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 0.
is_regression_task (bool): Whether it is a regression task. Defualts to False.
"""
def __init__(
self,
*,
audio_tar_filepaths: Union[str, List[str]],
manifest_filepath: str,
labels: List[str],
featurizer,
shuffle_n: int = 0,
min_duration: Optional[float] = 0.1,
max_duration: Optional[float] = None,
trim: bool = False,
shard_strategy: str = "scatter",
global_rank: int = 0,
world_size: int = 0,
is_regression_task: bool = False,
):
self.collection = collections.ASRSpeechLabel(
manifests_files=manifest_filepath.split(','),
min_duration=min_duration,
max_duration=max_duration,
index_by_file_id=True, # Must set this so the manifest lines can be indexed by file ID
)
self.file_occurence = count_occurence(self.collection.mapping)
self.featurizer = featurizer
self.trim = trim
self.labels = labels if labels else self.collection.uniq_labels
self.num_classes = len(self.labels)
self.label2id, self.id2label = {}, {}
for label_id, label in enumerate(self.labels):
self.label2id[label] = label_id
self.id2label[label_id] = label
for idx in range(len(self.labels[:5])):
logging.debug(" label id {} and its mapped label {}".format(idx, self.id2label[idx]))
valid_shard_strategies = ['scatter', 'replicate']
if shard_strategy not in valid_shard_strategies:
raise ValueError(f"`shard_strategy` must be one of {valid_shard_strategies}")
if isinstance(audio_tar_filepaths, str):
# Replace '(' and '[' with '{'
brace_keys_open = ['(', '[', '<', '_OP_']
for bkey in brace_keys_open:
if bkey in audio_tar_filepaths:
audio_tar_filepaths = audio_tar_filepaths.replace(bkey, "{")
# Replace ')' and ']' with '}'
brace_keys_close = [')', ']', '>', '_CL_']
for bkey in brace_keys_close:
if bkey in audio_tar_filepaths:
audio_tar_filepaths = audio_tar_filepaths.replace(bkey, "}")
# Check for distributed and partition shards accordingly
if world_size > 1:
if isinstance(audio_tar_filepaths, str):
# Brace expand
audio_tar_filepaths = list(braceexpand.braceexpand(audio_tar_filepaths))
if shard_strategy == 'scatter':
logging.info("All tarred dataset shards will be scattered evenly across all nodes.")
if len(audio_tar_filepaths) % world_size != 0:
logging.warning(
f"Number of shards in tarred dataset ({len(audio_tar_filepaths)}) is not divisible "
f"by number of distributed workers ({world_size})."
)
begin_idx = (len(audio_tar_filepaths) // world_size) * global_rank
end_idx = begin_idx + (len(audio_tar_filepaths) // world_size)
audio_tar_filepaths = audio_tar_filepaths[begin_idx:end_idx]
logging.info(
"Partitioning tarred dataset: process (%d) taking shards [%d, %d)", global_rank, begin_idx, end_idx
)
elif shard_strategy == 'replicate':
logging.info("All tarred dataset shards will be replicated across all nodes.")
else:
raise ValueError(f"Invalid shard strategy ! Allowed values are : {valid_shard_strategies}")
# Put together WebDataset
self._dataset = wd.WebDataset(urls=audio_tar_filepaths, nodesplitter=None)
if shuffle_n > 0:
self._dataset = self._dataset.shuffle(shuffle_n)
else:
logging.info("WebDataset will not shuffle files within the tar files.")
self._dataset = (
self._dataset.rename(audio=VALID_FILE_FORMATS, key='__key__')
.to_tuple('audio', 'key')
.pipe(self._filter)
.map(f=self._build_sample)
)
def _filter(self, iterator):
"""This function is used to remove samples that have been filtered out by ASRSpeechLabel already.
Otherwise, we would get a KeyError as _build_sample attempts to find the manifest entry for a sample
that was filtered out (e.g. for duration).
Note that if using multi-GPU training, filtering may lead to an imbalance in samples in each shard,
which may make your code hang as one process will finish before the other.
"""
class TarredAudioFilter:
def __init__(self, collection, file_occurence):
self.iterator = iterator
self.collection = collection
self.file_occurence = file_occurence
self._iterable = None
def __iter__(self):
self._iterable = self._internal_generator()
return self
def __next__(self):
try:
values = next(self._iterable)
except StopIteration:
# reset generator
self._iterable = self._internal_generator()
values = next(self._iterable)
return values
def _internal_generator(self):
"""
WebDataset requires an Iterator, but we require an iterable that yields 1-or-more
values per value inside self.iterator.
Therefore wrap the iterator with a generator function that will yield 1-or-more
values per sample in the iterator.
"""
for _, tup in enumerate(self.iterator):
audio_bytes, audio_filename = tup
file_id, _ = os.path.splitext(os.path.basename(audio_filename))
if audio_filename in self.file_occurence:
for j in range(0, self.file_occurence[file_id]):
if j == 0:
audio_filename = file_id
else:
audio_filename = file_id + "-sub" + str(j)
yield audio_bytes, audio_filename
return TarredAudioFilter(self.collection, self.file_occurence)
def _build_sample(self, tup):
"""Builds the training sample by combining the data from the WebDataset with the manifest info.
"""
audio_bytes, audio_filename = tup
# Grab manifest entry from self.collection
file_id, _ = os.path.splitext(os.path.basename(audio_filename))
manifest_idx = self.collection.mapping[file_id]
manifest_entry = self.collection[manifest_idx]
offset = manifest_entry.offset
if offset is None:
offset = 0
# Convert audio bytes to IO stream for processing (for SoundFile to read)
audio_filestream = io.BytesIO(audio_bytes)
features = self.featurizer.process(
audio_filestream, offset=offset, duration=manifest_entry.duration, trim=self.trim,
)
audio_filestream.close()
# Audio features
f, fl = features, torch.tensor(features.shape[0]).long()
t = self.label2id[manifest_entry.label]
tl = 1 # For compatibility with collate_fn used later
return f, fl, torch.tensor(t).long(), torch.tensor(tl).long()
def __iter__(self):
return self._dataset.__iter__()
def __len__(self):
return len(self.collection)
class TarredAudioToClassificationLabelDataset(_TarredAudioLabelDataset):
"""
A similar Dataset to the AudioToClassificationLabelDataset, but which loads tarred audio files.
Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToClassificationLabelDataset),
as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should
contain the information for one audio file, including at least the transcript and name of the audio
file within the tarball.
Valid formats for the audio_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or
(2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...].
See the WebDataset documentation for more information about accepted data and input formats.
If using multiple processes the number of shards should be divisible by the number of workers to ensure an
even split among workers. If it is not divisible, logging will give a warning but training will proceed.
In addition, if using mutiprocessing, each shard MUST HAVE THE SAME NUMBER OF ENTRIES after filtering
is applied. We currently do not check for this, but your program may hang if the shards are uneven!
Notice that a few arguments are different from the AudioToBPEDataset; for example, shuffle (bool) has been
replaced by shuffle_n (int).
Additionally, please note that the len() of this DataLayer is assumed to be the length of the manifest
after filtering. An incorrect manifest length may lead to some DataLoader issues down the line.
Args:
audio_tar_filepaths: Either a list of audio tarball filepaths, or a
string (can be brace-expandable).
manifest_filepath (str): Path to the manifest.
labels (list): Dataset parameter.
List of target classes that can be output by the speaker recognition model.
featurizer
shuffle_n (int): How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
Defaults to 0.
min_duration (float): Dataset parameter.
All training files which have a duration less than min_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to 0.1.
max_duration (float): Dataset parameter.
All training files which have a duration more than max_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to None.
trim(bool): Whether to use trim silence from beginning and end
of audio signal using librosa.effects.trim().
Defaults to False.
shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp.
- `scatter`: The default shard strategy applied by WebDataset, where each node gets
a unique set of shards, which are permanently pre-allocated and never changed at runtime.
- `replicate`: Optional shard strategy, where each node gets all of the set of shards
available in the tarred dataset, which are permanently pre-allocated and never changed at runtime.
The benefit of replication is that it allows each node to sample data points from the entire
dataset independently of other nodes, and reduces dependence on value of `shuffle_n`.
Note: Replicated strategy allows every node to sample the entire set of available tarfiles,
and therefore more than one node may sample the same tarfile, and even sample the same
data points! As such, there is no assured guarantee that all samples in the dataset will be
sampled at least once during 1 epoch.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 0.
is_regression_task (bool): Whether it is a regression task. Defualts to False.
"""
# self.labels = labels if labels else self.collection.uniq_labels
# self.num_commands = len(self.labels)
def _collate_fn(self, batch):
return _speech_collate_fn(batch, pad_id=0)
class TarredAudioToSpeechLabelDataset(_TarredAudioLabelDataset):
"""
A similar Dataset to the AudioToSpeechLabelDataset, but which loads tarred audio files.
Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToSpeechLabelDataset),
as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should
contain the information for one audio file, including at least the transcript and name of the audio
file within the tarball.
Valid formats for the audio_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or
(2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...].
See the WebDataset documentation for more information about accepted data and input formats.
If using multiple processes the number of shards should be divisible by the number of workers to ensure an
even split among workers. If it is not divisible, logging will give a warning but training will proceed.
In addition, if using mutiprocessing, each shard MUST HAVE THE SAME NUMBER OF ENTRIES after filtering
is applied. We currently do not check for this, but your program may hang if the shards are uneven!
Notice that a few arguments are different from the AudioToBPEDataset; for example, shuffle (bool) has been
replaced by shuffle_n (int).
Additionally, please note that the len() of this DataLayer is assumed to be the length of the manifest
after filtering. An incorrect manifest length may lead to some DataLoader issues down the line.
Args:
audio_tar_filepaths: Either a list of audio tarball filepaths, or a
string (can be brace-expandable).
manifest_filepath (str): Path to the manifest.
labels (list): Dataset parameter.
List of target classes that can be output by the speaker recognition model.
featurizer
shuffle_n (int): How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
Defaults to 0.
min_duration (float): Dataset parameter.
All training files which have a duration less than min_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to 0.1.
max_duration (float): Dataset parameter.
All training files which have a duration more than max_duration
are dropped. Note: Duration is read from the manifest JSON.
Defaults to None.
trim(bool): Whether to use trim silence from beginning and end
of audio signal using librosa.effects.trim().
Defaults to False.
time_length (float): time length of slice (in seconds) # Pass this only for speaker recognition and VAD task
shift_length (float): amount of shift of window for generating the frame for VAD task. in a batch # Pass this only for VAD task during inference.
normalize_audio (bool): Whether to normalize audio signal. Defaults to False.
shard_strategy (str): Tarred dataset shard distribution strategy chosen as a str value during ddp.
- `scatter`: The default shard strategy applied by WebDataset, where each node gets
a unique set of shards, which are permanently pre-allocated and never changed at runtime.
- `replicate`: Optional shard strategy, where each node gets all of the set of shards
available in the tarred dataset, which are permanently pre-allocated and never changed at runtime.
The benefit of replication is that it allows each node to sample data points from the entire
dataset independently of other nodes, and reduces dependence on value of `shuffle_n`.
Note: Replicated strategy allows every node to sample the entire set of available tarfiles,
and therefore more than one node may sample the same tarfile, and even sample the same
data points! As such, there is no assured guarantee that all samples in the dataset will be
sampled at least once during 1 epoch.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 0.
"""
def __init__(
self,
*,
audio_tar_filepaths: Union[str, List[str]],
manifest_filepath: str,
labels: List[str],
featurizer,
shuffle_n: int = 0,
min_duration: Optional[float] = 0.1,
max_duration: Optional[float] = None,
trim: bool = False,
time_length: Optional[float] = 8,
shift_length: Optional[float] = 1,
normalize_audio: bool = False,
shard_strategy: str = "scatter",
global_rank: int = 0,
world_size: int = 0,
):
logging.info("Time length considered for collate func is {}".format(time_length))
logging.info("Shift length considered for collate func is {}".format(shift_length))
self.time_length = time_length
self.shift_length = shift_length
self.normalize_audio = normalize_audio
super().__init__(
audio_tar_filepaths=audio_tar_filepaths,
manifest_filepath=manifest_filepath,
labels=labels,
featurizer=featurizer,
shuffle_n=shuffle_n,
min_duration=min_duration,
max_duration=max_duration,
trim=trim,
shard_strategy=shard_strategy,
global_rank=global_rank,
world_size=world_size,
)
def fixed_seq_collate_fn(self, batch):
return _fixed_seq_collate_fn(self, batch)
def sliced_seq_collate_fn(self, batch):
return _sliced_seq_collate_fn(self, batch)
def vad_frame_seq_collate_fn(self, batch):
return _vad_frame_seq_collate_fn(self, batch)
@@ -0,0 +1,129 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.data import audio_to_label
def get_classification_label_dataset(featurizer, config: dict) -> audio_to_label.AudioToClassificationLabelDataset:
"""
Instantiates a Classification AudioLabelDataset.
Args:
config: Config of the AudioToClassificationLabelDataset.
Returns:
An instance of AudioToClassificationLabelDataset.
"""
dataset = audio_to_label.AudioToClassificationLabelDataset(
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
featurizer=featurizer,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
trim=config.get('trim_silence', False),
is_regression_task=config.get('is_regression_task', False),
)
return dataset
def get_speech_label_dataset(featurizer, config: dict) -> audio_to_label.AudioToSpeechLabelDataset:
"""
Instantiates a Speech Label (e.g. VAD, speaker recognition) AudioLabelDataset.
Args:
config: Config of the AudioToSpeechLabelDataSet.
Returns:
An instance of AudioToSpeechLabelDataset.
"""
dataset = audio_to_label.AudioToSpeechLabelDataset(
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
featurizer=featurizer,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
trim=config.get('trim_silence', False),
time_length=config.get('time_length', 0.31),
shift_length=config.get('shift_length', 0.01),
normalize_audio=config.get('normalize_audio', False),
)
return dataset
def get_tarred_classification_label_dataset(
featurizer, config: dict, shuffle_n: int, global_rank: int, world_size: int
) -> audio_to_label.TarredAudioToClassificationLabelDataset:
"""
Instantiates a Classification TarredAudioLabelDataset.
Args:
config: Config of the TarredAudioToClassificationLabelDataset.
shuffle_n: How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
global_rank: Global rank of this device.
world_size: Global world size in the training method.
Returns:
An instance of TarredAudioToClassificationLabelDataset.
"""
dataset = audio_to_label.TarredAudioToClassificationLabelDataset(
audio_tar_filepaths=config['tarred_audio_filepaths'],
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
featurizer=featurizer,
shuffle_n=shuffle_n,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
trim=config.get('trim_silence', False),
shard_strategy=config.get('tarred_shard_strategy', 'scatter'),
global_rank=global_rank,
world_size=world_size,
is_regression_task=config.get('is_regression_task', False),
)
return dataset
def get_tarred_speech_label_dataset(
featurizer, config: dict, shuffle_n: int, global_rank: int, world_size: int,
) -> audio_to_label.TarredAudioToSpeechLabelDataset:
"""
InInstantiates a Speech Label (e.g. VAD, speaker recognition) TarredAudioLabelDataset.
Args:
config: Config of the TarredAudioToSpeechLabelDataset.
shuffle_n: How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
global_rank: Global rank of this device.
world_size: Global world size in the training method.
Returns:
An instance of TarredAudioToSpeechLabelDataset.
"""
dataset = audio_to_label.TarredAudioToSpeechLabelDataset(
audio_tar_filepaths=config['tarred_audio_filepaths'],
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
featurizer=featurizer,
shuffle_n=shuffle_n,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
trim=config.get('trim_silence', False),
time_length=config.get('time_length', 0.31),
shift_length=config.get('shift_length', 0.01),
normalize_audio=config.get('normalize_audio', False),
shard_strategy=config.get('tarred_shard_strategy', 'scatter'),
global_rank=global_rank,
world_size=world_size,
)
return dataset
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,444 @@
# Copyright (c) 2020, 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 math
from collections.abc import Iterator
from typing import Callable, List, Optional, Union
import torch
from omegaconf import DictConfig
from nemo.collections.common.parts.preprocessing import parsers
from nemo.utils.decorators import experimental
try:
import nvidia.dali as dali
from nvidia.dali.pipeline import Pipeline
from nvidia.dali.plugin.pytorch import DALIGenericIterator as DALIPytorchIterator
from nvidia.dali.plugin.pytorch import LastBatchPolicy as LastBatchPolicy
HAVE_DALI = True
except (ImportError, ModuleNotFoundError):
HAVE_DALI = False
__all__ = [
'AudioToCharDALIDataset',
]
class DALIOutputs(object):
def __init__(self, out_dict):
self._has_processed_signal = 'processed_signal' in out_dict and 'processed_signal_len' in out_dict
if not self._has_processed_signal:
assert 'audio' in out_dict and 'audio_len' in out_dict
assert 'transcript' in out_dict and 'transcript_len' in out_dict
if self._has_processed_signal:
self._outs = (
out_dict['processed_signal'],
out_dict['processed_signal_len'].reshape(-1),
out_dict['transcript'],
out_dict['transcript_len'].reshape(-1),
)
else:
self._outs = (
out_dict['audio'],
out_dict['audio_len'].reshape(-1),
out_dict['transcript'],
out_dict['transcript_len'].reshape(-1),
)
@property
def has_processed_signal(self):
return self._has_processed_signal
def __getitem__(self, key):
return self._outs[key]
def __len__(self):
return len(self._outs)
@experimental
class AudioToCharDALIDataset(Iterator):
"""
NVIDIA DALI pipeline that loads tensors via one or more manifest files where each line containing a sample descriptor in JSON,
including audio files, transcripts, and durations (in seconds).
Here's an example:
{"audio_filepath": "/path/to/audio.wav", "text_filepath": "/path/to/audio.txt", "duration": 23.147}
...
{"audio_filepath": "/path/to/audio.wav", "text": "the transcription", "offset": 301.75, "duration": 0.82, "utt":
"utterance_id", "ctm_utt": "en_4156", "side": "A"}
Args:
manifest_filepath: Path to manifest file with the format described above. Can be comma-separated paths.
labels: String containing all the possible characters to map to.
sample_rate (int): Sample rate to resample loaded audio to.
batch_size (int): Number of samples in a batch.
num_threads (int): Number of CPU processing threads to be created by the DALI pipeline.
max_duration (float): Determines the maximum allowed duration, in seconds, of the loaded audio files.
min_duration (float): Determines the minimum allowed duration, in seconds, of the loaded audio files.
blank_index (int): blank character index, default = -1
unk_index (int): unk_character index, default = -1
normalize (bool): whether to normalize transcript text (default): True
bos_id (int): Id of beginning of sequence symbol to append if not None
eos_id (int): Id of end of sequence symbol to append if not None
trim (bool): If True, it will extract the nonsilent region of the loaded audio signal.
shuffle (bool): If set to True, the dataset will shuffled after loading.
drop_last (bool): If set to True, the last batch will be dropped if incomplete. This will be the case when the shard size is not divisible by the batch size.
If set to False and the size of dataset is not divisible by the batch size, then the last batch will be smaller.
device (str): Determines the device type to be used for preprocessing. Allowed values are: 'cpu', 'gpu'.
device_id (int): Index of the GPU to be used (local_rank). Only applicable when device == 'gpu'. Defaults to 0.
global_rank (int): Worker rank, used for partitioning shards. Defaults to 0.
world_size (int): Total number of processes, used for partitioning shards. Defaults to 1.
preprocessor_cfg (DictConfig): Preprocessor configuration. Supports AudioToMelSpectrogramPreprocessor and AudioToMFCCPreprocessor.
"""
def __init__(
self,
manifest_filepath: str,
device: str,
batch_size: int,
labels: Union[str, List[str]],
sample_rate: int = 16000,
num_threads: int = 4,
max_duration: float = 0.0,
min_duration: float = 0.0,
blank_index: int = -1,
unk_index: int = -1,
normalize: bool = True,
bos_id: Optional[int] = None,
eos_id: Optional[int] = None,
trim: bool = False,
shuffle: bool = True,
drop_last: bool = False,
parser: Union[str, Callable] = 'en',
device_id: int = 0,
global_rank: int = 0,
world_size: int = 1,
preprocessor_cfg: DictConfig = None,
):
self.drop_last = drop_last # used by lr_scheduler
if not HAVE_DALI:
raise ModuleNotFoundError(
f"{self} requires NVIDIA DALI to be installed. "
f"See: https://docs.nvidia.com/deeplearning/dali/user-guide/docs/installation.html#id1"
)
if device not in ('cpu', 'gpu'):
raise ValueError(
f"{self} received an unexpected device argument {device}. Supported values are: 'cpu', 'gpu'"
)
self.batch_size = batch_size # Used by NeMo
self.device = device
self.device_id = device_id
if world_size > 1:
self.shard_id = global_rank
self.num_shards = world_size
else:
self.shard_id = None
self.num_shards = None
self.labels = labels
if self.labels is None or len(self.labels) == 0:
raise ValueError(f"{self} expects non empty labels list")
self.parser = parsers.make_parser(
labels=labels, name=parser, unk_id=unk_index, blank_id=blank_index, do_normalize=normalize,
)
self.eos_id = eos_id
self.bos_id = bos_id
self.sample_rate = sample_rate
self.pipe = Pipeline(
batch_size=batch_size,
num_threads=num_threads,
device_id=self.device_id,
exec_async=True,
exec_pipelined=True,
)
has_preprocessor = preprocessor_cfg is not None
if has_preprocessor:
if preprocessor_cfg._target_ == "nemo.collections.asr.modules.AudioToMelSpectrogramPreprocessor":
feature_type = "mel_spectrogram"
elif preprocessor_cfg._target_ == "nemo.collections.asr.modules.AudioToMFCCPreprocessor":
feature_type = "mfcc"
else:
raise ValueError(
f"{self} received an unexpected preprocessor configuration: {preprocessor_cfg._target_}."
f" Supported preprocessors are: AudioToMelSpectrogramPreprocessor, AudioToMFCCPreprocessor"
)
# Default values taken from AudioToMelSpectrogramPreprocessor
params = preprocessor_cfg
self.dither = params['dither'] if 'dither' in params else 0.0
self.preemph = params['preemph'] if 'preemph' in params else 0.97
self.window_size_sec = params['window_size'] if 'window_size' in params else 0.02
self.window_stride_sec = params['window_stride'] if 'window_stride' in params else 0.01
self.sample_rate = params['sample_rate'] if 'sample_rate' in params else sample_rate
self.window_size = int(self.window_size_sec * self.sample_rate)
self.window_stride = int(self.window_size_sec * self.sample_rate)
normalize = params['normalize'] if 'normalize' in params else 'per_feature'
if normalize == 'per_feature': # Each freq channel independently
self.normalization_axes = (1,)
elif normalize == 'all_features':
self.normalization_axes = (0, 1)
else:
raise ValueError(
f"{self} received {normalize} for the normalize parameter."
f" It must be either 'per_feature' or 'all_features'."
)
self.window = None
window_name = params['window'] if 'window' in params else None
torch_windows = {
'hamming': torch.hamming_window,
'blackman': torch.blackman_window,
'bartlett': torch.bartlett_window,
}
if window_name is None or window_name == 'hann':
self.window = None # Hann is DALI's default
elif window_name == 'ones':
self.window = torch.ones(self.window_size)
else:
try:
window_fn = torch_windows.get(window_name, None)
self.window = window_fn(self.window_size, periodic=False)
except:
raise ValueError(
f"{self} received {window_name} for the window parameter."
f" It must be one of: ('hann', 'ones', 'hamming', 'blackman', 'bartlett', None)."
f" None is equivalent to 'hann'."
)
self.n_fft = params['n_fft'] if 'n_fft' in params else None # None means default
self.n_mels = params['n_mels'] if 'n_mels' in params else 64
self.n_mfcc = params['n_mfcc'] if 'n_mfcc' in params else 64
features = params['features'] if 'features' in params else 0
if features > 0:
if feature_type == 'mel_spectrogram':
self.n_mels = features
elif feature_type == 'mfcc':
self.n_mfcc = features
# TODO Implement frame splicing
if 'frame_splicing' in params:
assert params['frame_splicing'] == 1, "Frame splicing is not implemented"
self.freq_low = params['lowfreq'] if 'lowfreq' in params else 0.0
self.freq_high = params['highfreq'] if 'highfreq' in params else self.sample_rate / 2.0
self.log_features = params['log'] if 'log' in params else True
# We want to avoid taking the log of zero
# There are two options: either adding or clamping to a small value
self.log_zero_guard_type = params['log_zero_guard_type'] if 'log_zero_guard_type' in params else 'add'
if self.log_zero_guard_type not in ["add", "clamp"]:
raise ValueError(
f"{self} received {self.log_zero_guard_type} for the "
f"log_zero_guard_type parameter. It must be either 'add' or "
f"'clamp'."
)
self.log_zero_guard_value = params['log_zero_guard_value'] if 'log_zero_guard_value' in params else 1e-05
if isinstance(self.log_zero_guard_value, str):
if self.log_zero_guard_value == "tiny":
self.log_zero_guard_value = torch.finfo(torch.float32).tiny
elif self.log_zero_guard_value == "eps":
self.log_zero_guard_value = torch.finfo(torch.float32).eps
else:
raise ValueError(
f"{self} received {self.log_zero_guard_value} for the log_zero_guard_type parameter."
f"It must be either a number, 'tiny', or 'eps'"
)
self.mag_power = params['mag_power'] if 'mag_power' in params else 2
if self.mag_power != 1.0 and self.mag_power != 2.0:
raise ValueError(
f"{self} received {self.mag_power} for the mag_power parameter." f" It must be either 1.0 or 2.0."
)
self.pad_to = params['pad_to'] if 'pad_to' in params else 16
self.pad_value = params['pad_value'] if 'pad_value' in params else 0.0
with self.pipe:
audio, transcript = dali.fn.nemo_asr_reader(
name="Reader",
manifest_filepaths=manifest_filepath.split(','),
dtype=dali.types.FLOAT,
downmix=True,
sample_rate=float(self.sample_rate),
min_duration=min_duration,
max_duration=max_duration,
read_sample_rate=False,
read_text=True,
random_shuffle=shuffle,
shard_id=self.shard_id,
num_shards=self.num_shards,
pad_last_batch=False,
)
transcript_len = dali.fn.shapes(dali.fn.reshape(transcript, shape=[-1]))
transcript = dali.fn.pad(transcript)
# Extract nonsilent region, if necessary
if trim:
# Need to extract non-silent region before moving to the GPU
roi_start, roi_len = dali.fn.nonsilent_region(audio, cutoff_db=-60)
audio = audio.gpu() if self.device == 'gpu' else audio
audio = dali.fn.slice(
audio, roi_start, roi_len, normalized_anchor=False, normalized_shape=False, axes=[0]
)
else:
audio = audio.gpu() if self.device == 'gpu' else audio
if not has_preprocessor:
# No preprocessing, the output is the audio signal
audio_len = dali.fn.shapes(dali.fn.reshape(audio, shape=[-1]))
audio = dali.fn.pad(audio)
self.pipe.set_outputs(audio, audio_len, transcript, transcript_len)
else:
# Additive gaussian noise (dither)
if self.dither > 0.0:
gaussian_noise = dali.fn.normal_distribution(device=self.device)
audio = audio + self.dither * gaussian_noise
# Preemphasis filter
if self.preemph > 0.0:
audio = dali.fn.preemphasis_filter(audio, preemph_coeff=self.preemph)
# Power spectrogram
spec = dali.fn.spectrogram(
audio, nfft=self.n_fft, window_length=self.window_size, window_step=self.window_stride
)
if feature_type == 'mel_spectrogram' or feature_type == 'mfcc':
# Spectrogram to Mel Spectrogram
spec = dali.fn.mel_filter_bank(
spec,
sample_rate=self.sample_rate,
nfilter=self.n_mels,
normalize=True,
freq_low=self.freq_low,
freq_high=self.freq_high,
)
# Mel Spectrogram to MFCC
if feature_type == 'mfcc':
spec = dali.fn.mfcc(spec, n_mfcc=self.n_mfcc)
# Logarithm
if self.log_zero_guard_type == 'add':
spec = spec + self.log_zero_guard_value
spec = dali.fn.to_decibels(
spec, multiplier=math.log(10), reference=1.0, cutoff_db=math.log(self.log_zero_guard_value)
)
# Normalization
spec = dali.fn.normalize(spec, axes=self.normalization_axes)
# Extracting the length of the spectrogram
spec_len = dali.fn.slice(dali.fn.shapes(spec), 1, 1, axes=(0,))
# Pads feature dimension to be a multiple of `pad_to` and the temporal dimension to be as big as the largest sample (shape -1)
spec = dali.fn.pad(spec, fill_value=self.pad_value, axes=(0, 1), align=(self.pad_to, 1), shape=(1, -1))
self.pipe.set_outputs(spec, spec_len, transcript, transcript_len)
# Building DALI pipeline
self.pipe.build()
if has_preprocessor:
output_names = ['processed_signal', 'processed_signal_len', 'transcript_raw', 'transcript_raw_len']
else:
output_names = ['audio', 'audio_len', 'transcript_raw', 'transcript_raw_len']
last_batch_policy = LastBatchPolicy.DROP if drop_last else LastBatchPolicy.PARTIAL
self._iter = DALIPytorchIterator(
[self.pipe],
output_map=output_names,
reader_name="Reader",
last_batch_policy=last_batch_policy,
dynamic_shape=True,
auto_reset=True,
)
# TODO come up with a better solution
class DummyDataset:
def __init__(self, parent):
self.parent = parent
def __len__(self):
return self.parent.size
self.dataset = DummyDataset(self) # Used by NeMo
def reset(self):
self._iter.reset()
def __iter__(self):
return self
def next(self):
return self.__next__()
@property
def size(self):
return self._iter.size
def __len__(self):
return len(self._iter)
def __next__(self):
outputs = self._iter.next()
assert len(outputs) == 1
dali_out = outputs[0]
text_raw_len = dali_out['transcript_raw_len'].numpy()
text_raw = dali_out['transcript_raw'].numpy()
out = {}
out_names = ['processed_signal', 'processed_signal_len', 'audio', 'audio_len']
for out_name in out_names:
if out_name in dali_out:
out[out_name] = dali_out[out_name].detach().clone()
text_tokens = []
text_tokens_len = []
max_len = 0
batch_size = text_raw.shape[0]
for i, text in enumerate(text_raw):
n = text_raw_len[i][0]
tbytes = str(text[:n].tobytes(), encoding='utf8')
ttokens = self.parser(tbytes)
if self.bos_id is not None:
ttokens = [self.bos_id] + ttokens
if self.eos_id is not None:
ttokens = ttokens + [self.eos_id]
ttokens_len = len(ttokens)
text_tokens_len.append(ttokens_len)
text_tokens.append(ttokens)
if ttokens_len > max_len:
max_len = ttokens_len
transcript_out = torch.zeros(batch_size, max_len, dtype=torch.long)
for i, n in enumerate(text_tokens_len):
transcript_out[i, :n] = torch.tensor(text_tokens[i], dtype=torch.long)
transcript_len_out = torch.tensor(text_tokens_len, dtype=torch.long)
out['transcript'] = transcript_out
out['transcript_len'] = transcript_len_out
return DALIOutputs(out)
@@ -0,0 +1,247 @@
# Copyright (c) 2020, 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.
from typing import Optional
import torch
from omegaconf import DictConfig, open_dict
from nemo.collections.asr.data import audio_to_text, audio_to_text_dali
from nemo.utils import logging
def inject_dataloader_value_from_model_config(model_cfg: dict, dataloader_cfg: dict, key: str):
"""
Extracts the label set provided at the top level of the model, and propagates it to the dataloader
config.
Args:
model_cfg: A DictConfig representing the model's config.
dataloader_cfg: A DictConfig representing the individual data loader
key: A str value representing a key in the model_cfg whose value will be propagated to the
dataloader config.
"""
if key not in model_cfg:
logging.info(
f"Model level config does not container `{key}`, please explicitly provide `{key}` to the dataloaders."
)
return
# If key exists in the data loader config (either set explicitly or as a placeholder (via None))
if key in dataloader_cfg:
# Dataloader `labels` is provided and is non-null
if dataloader_cfg[key] is not None and model_cfg[key] != dataloader_cfg[key]:
# Model level `labels` dont match Dataloader level `labels`
logging.warning(
f'`{key}` is explicitly provided to the data loader, and is different from '
f'the `{key}` provided at the model level config.\n'
f'If this is incorrect, please set the dataloader\'s `{key}` to None.'
)
else:
# Dataloader `key` is None or values match
# Propagate from model level `key` (even if they match)
with open_dict(dataloader_cfg):
dataloader_cfg[key] = model_cfg[key]
else:
# If key key doesnt even exist in dataloader_cfg, inject it explicitly
with open_dict(dataloader_cfg):
dataloader_cfg[key] = model_cfg[key]
def get_char_dataset(config: dict, augmentor: Optional['AudioAugmentor'] = None) -> audio_to_text.AudioToCharDataset:
"""
Instantiates a Character Encoding based AudioToCharDataset.
Args:
config: Config of the AudioToCharDataset.
augmentor: Optional AudioAugmentor object for augmentations on audio data.
Returns:
An instance of AudioToCharDataset.
"""
dataset = audio_to_text.AudioToCharDataset(
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
sample_rate=config['sample_rate'],
int_values=config.get('int_values', False),
augmentor=augmentor,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
max_utts=config.get('max_utts', 0),
blank_index=config.get('blank_index', -1),
unk_index=config.get('unk_index', -1),
normalize=config.get('normalize_transcripts', False),
trim=config.get('trim_silence', False),
parser=config.get('parser', 'en'),
)
return dataset
def get_bpe_dataset(
config: dict, tokenizer: 'TokenizerSpec', augmentor: Optional['AudioAugmentor'] = None
) -> audio_to_text.AudioToBPEDataset:
"""
Instantiates a Byte Pair Encoding / Word Piece Encoding based AudioToBPEDataset.
Args:
config: Config of the AudioToBPEDataset.
tokenizer: An instance of a TokenizerSpec object.
augmentor: Optional AudioAugmentor object for augmentations on audio data.
Returns:
An instance of AudioToBPEDataset.
"""
dataset = audio_to_text.AudioToBPEDataset(
manifest_filepath=config['manifest_filepath'],
tokenizer=tokenizer,
sample_rate=config['sample_rate'],
int_values=config.get('int_values', False),
augmentor=augmentor,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
max_utts=config.get('max_utts', 0),
trim=config.get('trim_silence', False),
use_start_end_token=config.get('use_start_end_token', True),
)
return dataset
def get_tarred_char_dataset(
config: dict, shuffle_n: int, global_rank: int, world_size: int, augmentor: Optional['AudioAugmentor'] = None
) -> audio_to_text.TarredAudioToCharDataset:
"""
Instantiates a Character Encoding based TarredAudioToCharDataset.
Args:
config: Config of the TarredAudioToCharDataset.
shuffle_n: How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
global_rank: Global rank of this device.
world_size: Global world size in the training method.
augmentor: Optional AudioAugmentor object for augmentations on audio data.
Returns:
An instance of TarredAudioToCharDataset.
"""
dataset = audio_to_text.TarredAudioToCharDataset(
audio_tar_filepaths=config['tarred_audio_filepaths'],
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
sample_rate=config['sample_rate'],
int_values=config.get('int_values', False),
augmentor=augmentor,
shuffle_n=shuffle_n,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
max_utts=config.get('max_utts', 0),
blank_index=config.get('blank_index', -1),
unk_index=config.get('unk_index', -1),
normalize=config.get('normalize_transcripts', False),
trim=config.get('trim_silence', False),
parser=config.get('parser', 'en'),
shard_strategy=config.get('tarred_shard_strategy', 'scatter'),
global_rank=global_rank,
world_size=world_size,
)
return dataset
def get_tarred_bpe_dataset(
config: dict,
tokenizer: 'TokenizerSpec',
shuffle_n: int,
global_rank: int,
world_size: int,
augmentor: Optional['AudioAugmentor'] = None,
) -> audio_to_text.TarredAudioToBPEDataset:
"""
Instantiates a Byte Pair Encoding / Word Piece Encoding based TarredAudioToBPEDataset.
Args:
config: Config of the TarredAudioToBPEDataset.
tokenizer: An instance of a TokenizerSpec object.
shuffle_n: How many samples to look ahead and load to be shuffled.
See WebDataset documentation for more details.
global_rank: Global rank of this device.
world_size: Global world size in the training method.
augmentor: Optional AudioAugmentor object for augmentations on audio data.
Returns:
An instance of TarredAudioToBPEDataset.
"""
dataset = audio_to_text.TarredAudioToBPEDataset(
audio_tar_filepaths=config['tarred_audio_filepaths'],
manifest_filepath=config['manifest_filepath'],
tokenizer=tokenizer,
sample_rate=config['sample_rate'],
int_values=config.get('int_values', False),
augmentor=augmentor,
shuffle_n=shuffle_n,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
max_utts=config.get('max_utts', 0),
trim=config.get('trim_silence', False),
use_start_end_token=config.get('use_start_end_token', True),
shard_strategy=config.get('tarred_shard_strategy', 'scatter'),
global_rank=global_rank,
world_size=world_size,
)
return dataset
def get_dali_char_dataset(
config: dict,
shuffle: bool,
device_id: int,
global_rank: int,
world_size: int,
preprocessor_cfg: Optional[DictConfig] = None,
) -> audio_to_text_dali.AudioToCharDALIDataset:
"""
Instantiates a Character Encoding based AudioToCharDALIDataset.
Args:
config: Config of the AudioToCharDALIDataset.
shuffle: Bool flag whether to shuffle the dataset.
device_id: Index of the GPU to be used (local_rank). Only applicable when device == 'gpu'. Defaults to 0.
global_rank: Global rank of this device.
world_size: Global world size in the training method.
augmentor: Optional AudioAugmentor object for augmentations on audio data.
Returns:
An instance of AudioToCharDALIDataset.
"""
device = 'gpu' if torch.cuda.is_available() else 'cpu'
dataset = audio_to_text_dali.AudioToCharDALIDataset(
manifest_filepath=config['manifest_filepath'],
device=device,
batch_size=config['batch_size'],
labels=config['labels'],
sample_rate=config['sample_rate'],
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
blank_index=config.get('blank_index', -1),
unk_index=config.get('unk_index', -1),
normalize=config.get('normalize_transcripts', False),
trim=config.get('trim_silence', False),
parser=config.get('parser', 'en'),
shuffle=shuffle,
device_id=device_id,
global_rank=global_rank,
world_size=world_size,
preprocessor_cfg=preprocessor_cfg,
)
return dataset
@@ -0,0 +1,139 @@
# Copyright (c) 2020, 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.
from typing import Dict, List, Optional
import torch
from nemo.collections.common.parts.preprocessing import collections
from nemo.core.classes import Dataset
from nemo.core.neural_types import AcousticEncodedRepresentation, LabelsType, LengthsType, NeuralType
from nemo.utils import logging
def _feature_collate_fn(batch):
"""collate batch of feat sig, feat len, tokens, tokens len
Args:
batch (FloatTensor, LongTensor, LongTensor, LongTensor): A tuple of tuples of feature, feature lengths,
encoded tokens, and encoded tokens length.
"""
_, feat_lengths, _, tokens_lengths = zip(*batch)
feat_signal, tokens = [], []
for sig, sig_len, tokens_i, tokens_i_len in batch:
feat_signal.append(sig)
tokens.append(tokens_i)
feat_signal = torch.stack(feat_signal)
feat_lengths = torch.stack(feat_lengths)
tokens = torch.stack(tokens)
tokens_lengths = torch.stack(tokens_lengths)
return feat_signal, feat_lengths, tokens, tokens_lengths
class _FeatureSeqSpeakerLabelDataset(Dataset):
"""
Dataset that loads tensors via a json file containing paths to feature files, sequences of labels.
Each new line is a different sample. Example below:
and their target labels. JSON files should be of the following format:
{"feature_filepath": "/path/to/feature_0.p", "seq_label": speakerA speakerB SpeakerA ....} \
...
{"feature_filepath": "/path/to/feature_n.p", "seq_label": target_seq_label_n}
target_seq_label_n is the string of sequence of speaker label, separated by space.
Args:
manifest_filepath (str): Dataset parameter. Path to JSON containing data.
labels (Optional[list]): Dataset parameter. List of unique labels collected from all samples.
feature_loader : Dataset parameter. Feature loader to load (external) feature.
"""
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
"""Returns definitions of module output ports.
"""
# TODO output type for external features
output_types = {
'external_feat': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
'feat_length': NeuralType(tuple('B'), LengthsType()),
}
if self.is_speaker_emb:
output_types.update(
{
'embs': NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
'embs_length': NeuralType(tuple('B'), LengthsType()),
'label': NeuralType(('B', 'T'), LabelsType()),
'label_length': NeuralType(tuple('B'), LengthsType()),
}
)
else:
output_types.update(
{'label': NeuralType(('B', 'T'), LabelsType()), 'label_length': NeuralType(tuple('B'), LengthsType()),}
)
return output_types
def __init__(
self, *, manifest_filepath: str, labels: List[str], feature_loader, is_speaker_emb: bool = False,
):
super().__init__()
self.collection = collections.ASRFeatureSequenceLabel(manifests_files=manifest_filepath.split(','),)
self.feature_loader = feature_loader
self.labels = labels if labels else self.collection.uniq_labels
self.is_speaker_emb = is_speaker_emb
self.label2id, self.id2label = {}, {}
for label_id, label in enumerate(self.labels):
self.label2id[label] = label_id
self.id2label[label_id] = label
for idx in range(len(self.labels[:5])):
logging.debug(" label id {} and its mapped label {}".format(idx, self.id2label[idx]))
def __len__(self):
return len(self.collection)
def __getitem__(self, index):
sample = self.collection[index]
features = self.feature_loader.process(sample.feature_file)
f, fl = features, torch.tensor(features.shape[0]).long()
t = torch.tensor(sample.seq_label).float()
tl = torch.tensor(len(sample.seq_label)).long()
return f, fl, t, tl
class FeatureToSeqSpeakerLabelDataset(_FeatureSeqSpeakerLabelDataset):
"""
Dataset that loads tensors via a json file containing paths to feature
files and sequence of speakers. Each new line is a
different sample. Example below:
{"feature_filepath": "/path/to/feature_0.p", "seq_label": speakerA speakerB SpeakerA ....} \
...
{"feature_filepath": "/path/to/feature_n.p", "seq_label": target_seq_label_n}
target_seq_label_n is the string of sequence of speaker label, separated by space.
Args:
manifest_filepath (str): Path to manifest json as described above. Canbe comma-separated paths.
labels (Optional[list]): String containing all the possible labels to map to
if None then automatically picks from ASRFeatureSequenceLabel collection.
feature_loader, Feature load to loader (external) feature.
"""
def _collate_fn(self, batch):
return _feature_collate_fn(batch)
@@ -0,0 +1,31 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.data import feature_to_label
def get_feature_seq_speakerlabel_dataset(
feature_loader, config: dict
) -> feature_to_label.FeatureToSeqSpeakerLabelDataset:
"""
Instantiates a FeatureSeqSpeakerLabelDataset.
Args:
config: Config of the FeatureToSeqSpeakerLabelDataset.
Returns:
An instance of FeatureToSeqSpeakerLabelDataset.
"""
dataset = feature_to_label.FeatureToSeqSpeakerLabelDataset(
manifest_filepath=config['manifest_filepath'], labels=config['labels'], feature_loader=feature_loader,
)
return dataset
+352
View File
@@ -0,0 +1,352 @@
# Copyright (c) 2020, 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 abc
import itertools
import re
import string
import unicodedata
from builtins import str as unicode
from typing import List
import nltk
from nemo.collections.common.parts.preprocessing import parsers
try:
import g2p_en # noqa
_g2p = g2p_en.G2p()
_g2p.variables = None
HAVE_G2P = True
except (FileNotFoundError, LookupError):
HAVE_G2P = False
_words_re = re.compile("([a-z\-]+'[a-z\-]+|[a-z\-]+)|([^a-z{}]+)")
def _text_preprocessing(text):
text = unicode(text)
text = ''.join(char for char in unicodedata.normalize('NFD', text) if unicodedata.category(char) != 'Mn')
text = text.lower()
text = re.sub("[^ a-z'\".,?!()\[\]:;\-]", "", text)
return text
def _word_tokenize(text):
words = _words_re.findall(text)
words = [re.sub(r'\s(\d)', r'\1', word[1].upper()) if word[0] == '' else word[0] for word in words]
return words
class G2p:
def __init__(
self,
phoneme_dict_path=None,
use_seq2seq_for_oov=False,
ignore_ambiguous_words=True,
text_preprocessing_func=_text_preprocessing,
word_tokenize_func=_word_tokenize,
):
# Download NLTK datasets if this class is to be instantiated
try:
nltk.data.find('taggers/averaged_perceptron_tagger.zip')
except LookupError:
nltk.download('averaged_perceptron_tagger', quiet=True)
try:
nltk.data.find('corpora/cmudict.zip')
except LookupError:
nltk.download('cmudict', quiet=True)
self.homograph2features = _g2p.homograph2features
self.g2p_dict = self._construct_grapheme2phoneme_dict(phoneme_dict_path)
self.use_seq2seq_for_oov = use_seq2seq_for_oov
self.ignore_ambiguous_words = ignore_ambiguous_words
self.text_preprocessing_func = text_preprocessing_func
self.word_tokenize_func = word_tokenize_func
@staticmethod
def _construct_grapheme2phoneme_dict(phoneme_dict_path=None, encoding='latin-1'):
if phoneme_dict_path is None:
from nltk.corpus import cmudict
return cmudict.dict()
_alt_re = re.compile(r'\([0-9]+\)')
g2p_dict = {}
with open(phoneme_dict_path, encoding=encoding) as file:
for line in file:
if len(line) and ('A' <= line[0] <= 'Z' or line[0] == "'"):
parts = line.split(' ')
word = re.sub(_alt_re, '', parts[0])
word = word.lower()
pronunciation = parts[1].strip().split(" ")
if word in g2p_dict:
g2p_dict[word].append(pronunciation)
else:
g2p_dict[word] = [pronunciation]
return g2p_dict
def handle_ambiguous(self, word):
if not self.ignore_ambiguous_words or len(self.g2p_dict[word]) == 1:
return True
return False
def __call__(self, text):
text = self.text_preprocessing_func(text)
words = self.word_tokenize_func(text)
words_and_pos_tags = nltk.pos_tag(words)
prons = []
for word, pos in words_and_pos_tags:
word_by_hyphen = word.split("-")
# punctuation
if re.search("[a-zA-Z]", word) is None:
pron = list(word)
# homograph
elif word in self.homograph2features:
pron1, pron2, pos1 = self.homograph2features[word]
if pos.startswith(pos1):
pron = pron1
else:
pron = pron2
# `'s` suffix
elif (
len(word) > 2
and word.endswith("'s")
and (word not in self.g2p_dict)
and (word[:-2] in self.g2p_dict)
and self.handle_ambiguous(word[:-2])
):
pron = self.g2p_dict[word[:-2]][0] + ["Z"]
# `s` suffix
elif (
len(word) > 1
and word.endswith("s")
and (word not in self.g2p_dict)
and (word[:-1] in self.g2p_dict)
and self.handle_ambiguous(word[:-1])
):
pron = self.g2p_dict[word[:-1]][0] + ["Z"]
# g2p dict
elif word in self.g2p_dict and self.handle_ambiguous(word):
pron = self.g2p_dict[word][0]
# word with hyphens
elif len(word_by_hyphen) > 1 and all(
[sub_word in self.g2p_dict and self.handle_ambiguous(sub_word) for sub_word in word_by_hyphen]
):
pron = []
for sub_word in word_by_hyphen:
pron.extend(self.g2p_dict[sub_word][0])
pron.extend(["-"])
pron.pop()
else:
if self.use_seq2seq_for_oov:
# run gru-based seq2seq model from _g2p
pron = _g2p.predict(word)
else:
pron = word
prons.extend(pron)
return prons
class Base(abc.ABC):
"""Vocabulary for turning str text to list of int tokens."""
# fmt: off
PUNCT = ( # Derived from LJSpeech and "/" additionally
',', '.', '!', '?', '-',
':', ';', '/', '"', '(',
')', '[', ']', '{', '}',
)
# fmt: on
PAD, BLANK, OOV = '<pad>', '<blank>', '<oov>'
def __init__(self, labels, *, pad=PAD, blank=BLANK, oov=OOV, sep='', add_blank_at="last_but_one"):
super().__init__()
labels = list(labels)
self.pad, labels = len(labels), labels + [pad] # Padding
if add_blank_at is not None:
self.blank, labels = len(labels), labels + [blank] # Reserved for blank from QN
else:
# use add_blank_at=None only for ASR where blank is added automatically
self.blank = -1
self.oov, labels = len(labels), labels + [oov] # Out Of Vocabulary
if add_blank_at == "last":
labels[-1], labels[-2] = labels[-2], labels[-1]
self.oov, self.blank = self.blank, self.oov
self.labels = labels
self.sep = sep
self._util_ids = {self.pad, self.blank, self.oov}
self._label2id = {l: i for i, l in enumerate(labels)}
self._id2label = labels
def __call__(self, text: str) -> List[int]:
return self.encode(text)
@abc.abstractmethod
def encode(self, text: str) -> List[int]:
"""Turns str text into int tokens."""
pass
def decode(self, tokens: List[int]) -> str:
"""Turns ints tokens into str text."""
return self.sep.join(self._id2label[t] for t in tokens if t not in self._util_ids)
class Chars(Base):
"""Chars vocabulary."""
def __init__(
self, punct=True, spaces=False, apostrophe=True, add_blank_at="last_but_one",
):
labels = []
self.space, labels = len(labels), labels + [' '] # Space
labels.extend(string.ascii_lowercase)
if apostrophe:
labels.append("'") # Apostrophe for saving "don't" and "Joe's"
if punct:
labels.extend(self.PUNCT)
super().__init__(labels, add_blank_at=add_blank_at)
self.punct = punct
self.spaces = spaces
self._parser = parsers.ENCharParser(labels)
def encode(self, text):
"""See base class."""
text = self._parser._normalize(text) # noqa
if self.spaces:
for p in set(text) & set(self.PUNCT):
text = text.replace(p, f' {p} ')
text = text.strip().replace(' ', ' ')
return self._parser._tokenize(text) # noqa
class Phonemes(Base):
"""Phonemes vocabulary."""
# fmt: off
VOWELS = (
'AA', 'AE', 'AH', 'AO', 'AW',
'AY', 'EH', 'ER', 'EY', 'IH',
'IY', 'OW', 'OY', 'UH', 'UW',
)
CONSONANTS = (
'B', 'CH', 'D', 'DH', 'F', 'G',
'HH', 'JH', 'K', 'L', 'M', 'N',
'NG', 'P', 'R', 'S', 'SH', 'T',
'TH', 'V', 'W', 'Y', 'Z', 'ZH',
)
# fmt: on
def __init__(
self,
punct=True,
stresses=False,
spaces=True,
chars=False,
*,
space=' ',
silence=None,
apostrophe=True,
oov=Base.OOV,
sep='|', # To be able to distinguish between 2/3 letters codes.
add_blank_at="last_but_one",
pad_with_space=False,
improved_version_g2p=False,
phoneme_dict_path=None,
):
labels = []
self.space, labels = len(labels), labels + [space] # Space
if silence is not None:
self.silence, labels = len(labels), labels + [silence] # Silence
labels.extend(self.CONSONANTS)
vowels = list(self.VOWELS)
if stresses:
vowels = [f'{p}{s}' for p, s in itertools.product(vowels, (0, 1, 2))]
labels.extend(vowels)
if chars:
labels.extend(string.ascii_lowercase)
if apostrophe:
labels.append("'") # Apostrophe
if punct:
labels.extend(self.PUNCT)
super().__init__(labels, oov=oov, sep=sep, add_blank_at=add_blank_at)
self.punct = punct
self.stresses = stresses
self.spaces = spaces
self.pad_with_space = pad_with_space
if improved_version_g2p:
self.g2p = G2p(phoneme_dict_path)
else:
self.g2p = _g2p
def encode(self, text):
"""See base class."""
ps, space, labels = [], self.labels[self.space], set(self.labels)
for p in self.g2p(text): # noqa
# Remove stress
if p.isalnum() and len(p) == 3 and not self.stresses:
p = p[:2]
# Add space if last one isn't one
if p == space and len(ps) > 0 and ps[-1] != space:
ps.append(p)
# Add next phoneme
if (p.isalnum() or p == "'") and p in labels:
ps.append(p)
# Add punct and remove space if needed
if (p in self.PUNCT) and self.punct:
if not self.spaces and len(ps) > 0 and ps[-1] == space:
ps.pop()
ps.append(p)
# Remove trailing spaces
while ps[-1] == space:
ps.pop()
if self.pad_with_space:
ps = [space] + ps + [space]
return [self._label2id[p] for p in ps]
@@ -0,0 +1,16 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.losses.angularloss import AngularSoftmaxLoss
from nemo.collections.asr.losses.ctc import CTCLoss
@@ -0,0 +1,68 @@
# ! /usr/bin/python
# Copyright (c) 2020, 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 torch
from nemo.core.classes import Loss, Typing, typecheck
from nemo.core.neural_types import LabelsType, LogitsType, LossType, NeuralType
__all__ = ['AngularSoftmaxLoss']
class AngularSoftmaxLoss(Loss, Typing):
"""
Computes ArcFace Angular softmax angle loss
reference: https://openaccess.thecvf.com/content_CVPR_2019/papers/Deng_ArcFace_Additive_Angular_Margin_Loss_for_Deep_Face_Recognition_CVPR_2019_paper.pdf
args:
scale: scale value for cosine angle
margin: margin value added to cosine angle
"""
@property
def input_types(self):
"""Input types definitions for AnguarLoss.
"""
return {
"logits": NeuralType(('B', 'D'), LogitsType()),
"labels": NeuralType(('B',), LabelsType()),
}
@property
def output_types(self):
"""Output types definitions for AngularLoss.
loss:
NeuralType(None)
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, scale=20.0, margin=1.35):
super().__init__()
self.eps = 1e-7
self.scale = scale
self.margin = margin
@typecheck()
def forward(self, logits, labels):
numerator = self.scale * torch.cos(
torch.acos(torch.clamp(torch.diagonal(logits.transpose(0, 1)[labels]), -1.0 + self.eps, 1 - self.eps))
+ self.margin
)
excl = torch.cat(
[torch.cat((logits[i, :y], logits[i, y + 1 :])).unsqueeze(0) for i, y in enumerate(labels)], dim=0
)
denominator = torch.exp(numerator) + torch.sum(torch.exp(self.scale * excl), dim=1)
L = numerator - torch.log(denominator)
return -torch.mean(L)
+70
View File
@@ -0,0 +1,70 @@
# ! /usr/bin/python
# Copyright (c) 2020, 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 torch
from torch import nn
from nemo.core.classes import Serialization, Typing, typecheck
from nemo.core.neural_types import LabelsType, LengthsType, LogprobsType, LossType, NeuralType
__all__ = ['CTCLoss']
class CTCLoss(nn.CTCLoss, Serialization, Typing):
@property
def input_types(self):
"""Input types definitions for CTCLoss.
"""
return {
"log_probs": NeuralType(('B', 'T', 'D'), LogprobsType()),
"targets": NeuralType(('B', 'T'), LabelsType()),
"input_lengths": NeuralType(tuple('B'), LengthsType()),
"target_lengths": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Output types definitions for CTCLoss.
loss:
NeuralType(None)
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, num_classes, zero_infinity=False, reduction='mean_batch'):
self._blank = num_classes
# Don't forget to properly call base constructor
if reduction == 'mean_batch':
ctc_reduction = 'none'
self._apply_batch_mean = True
elif reduction in ['sum', 'mean', 'none']:
ctc_reduction = reduction
self._apply_batch_mean = False
super().__init__(blank=self._blank, reduction=ctc_reduction, zero_infinity=zero_infinity)
@typecheck()
def forward(self, log_probs, targets, input_lengths, target_lengths):
# override forward implementation
# custom logic, if necessary
input_lengths = input_lengths.long()
target_lengths = target_lengths.long()
targets = targets.long()
# here we transpose because we expect [B, T, D] while PyTorch assumes [T, B, D]
log_probs = log_probs.transpose(1, 0)
loss = super().forward(
log_probs=log_probs, targets=targets, input_lengths=input_lengths, target_lengths=target_lengths
)
if self._apply_batch_mean:
loss = torch.mean(loss)
return loss
+288
View File
@@ -0,0 +1,288 @@
# ! /usr/bin/python
# Copyright (c) 2020, 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.
# Copyright 2018-2019, Mingkun Huang
#
# 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 operator
from dataclasses import dataclass
from typing import Optional
import torch
from nemo.core.classes import Loss, typecheck
from nemo.core.neural_types import LabelsType, LengthsType, LogprobsType, LossType, NeuralType
from nemo.core.utils.numba_utils import NUMBA_INSTALLATION_MESSAGE
from nemo.utils import logging, model_utils
try:
import warprnnt_pytorch as warprnnt
WARP_RNNT_AVAILABLE = True
except (ImportError, ModuleNotFoundError):
WARP_RNNT_AVAILABLE = False
try:
from nemo.collections.asr.parts.numba.rnnt_loss import RNNTLossNumba
NUMBA_RNNT_AVAILABLE = True
except (ImportError, ModuleNotFoundError):
NUMBA_RNNT_AVAILABLE = False
WARP_RNNT_INSTALLATION_MESSAGE = (
"Could not import `warprnnt_pytorch`.\n"
"Please visit https://github.com/HawkAaron/warp-transducer "
"and follow the steps in the readme to build and install the "
"pytorch bindings for RNNT Loss, or use the provided docker "
"container that supports RNN-T loss."
)
@dataclass
class RNNTLossConfig:
loss_name: str
lib_name: str
is_available: bool = False
installation_msg: str = ""
min_version: Optional[str] = None
# Resolved list of available RNNT losses
RNNT_LOSS_RESOLVER = {
"warprnnt": RNNTLossConfig(
loss_name="warprnnt",
lib_name="warprnnt_pytorch",
is_available=WARP_RNNT_AVAILABLE,
installation_msg=WARP_RNNT_INSTALLATION_MESSAGE,
),
"warprnnt_numba": RNNTLossConfig(
loss_name="warprnnt_numba",
lib_name="numba",
min_version='0.53.0',
is_available=NUMBA_RNNT_AVAILABLE,
installation_msg=NUMBA_INSTALLATION_MESSAGE,
),
}
RNNT_LOSS_RESOLVER['default'] = RNNT_LOSS_RESOLVER['warprnnt_numba']
def _warn_unused_additional_kwargs(loss_name, kwargs):
if len(kwargs) > 0:
logging.warning(
f"Loss function `{loss_name}` was provided with following additional kwargs,\n"
f"however they were ignored as it is unused.\n"
f"{kwargs}"
)
def resolve_rnnt_default_loss_name() -> str:
return RNNT_LOSS_RESOLVER['default'].loss_name
def resolve_rnnt_loss(loss_name: str, blank_idx: int, loss_kwargs: dict = None) -> torch.nn.Module:
loss_function_names = list(RNNT_LOSS_RESOLVER.keys())
if loss_name not in loss_function_names:
raise ValueError(
f"Provided `loss_name` {loss_name} not in list of available RNNT losses \n" f"{loss_function_names}"
)
all_available_losses = {name: config for name, config in RNNT_LOSS_RESOLVER.items() if config.is_available}
loss_config = RNNT_LOSS_RESOLVER[loss_name] # type: RNNTLossConfig
# Re-raise import error with installation message
if not loss_config.is_available:
msg = (
f"Installed RNNT losses are : {list(all_available_losses.keys())}.\n"
f"****************************************************************\n"
f"To install the selected loss function, please follow the steps below:\n"
f"{loss_config.installation_msg}"
)
raise ImportError(msg)
# Library version check
if loss_config.min_version is not None:
ver_matched, msg = model_utils.check_lib_version(
loss_config.lib_name, checked_version=loss_config.min_version, operator=operator.ge
)
if ver_matched is False:
msg = (
f"{msg}\n"
f"****************************************************************\n"
f"To update the selected loss function, please follow the steps below:\n"
f"{loss_config.installation_msg}"
)
raise RuntimeError(msg)
# Resolve loss functions sequentially
loss_kwargs = {} if loss_kwargs is None else loss_kwargs
# Get actual loss name for `default`
if loss_name == 'default':
loss_name = loss_config.loss_name
"""
Resolve RNNT loss functions
"""
if loss_name == 'warprnnt':
loss_func = warprnnt.RNNTLoss(blank=blank_idx, reduction='none')
_warn_unused_additional_kwargs(loss_name, loss_kwargs)
elif loss_name == 'warprnnt_numba':
loss_func = RNNTLossNumba(blank=blank_idx, reduction='none')
_warn_unused_additional_kwargs(loss_name, loss_kwargs)
else:
raise ValueError(
f"Invalid value of `loss_name`: {loss_name}. Allowed loss names are :" f"{loss_function_names}"
)
return loss_func
class RNNTLoss(Loss):
@property
def input_types(self):
"""Input types definitions for CTCLoss.
"""
return {
"log_probs": NeuralType(('B', 'T', 'D', 'D'), LogprobsType()),
"targets": NeuralType(('B', 'T'), LabelsType()),
"input_lengths": NeuralType(tuple('B'), LengthsType()),
"target_lengths": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Output types definitions for CTCLoss.
loss:
NeuralType(None)
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, num_classes, reduction: str = 'mean_batch', loss_name: str = "default", loss_kwargs=None):
"""
RNN-T Loss function based on https://github.com/HawkAaron/warp-transducer.
Optionally, can utilize a numba implementation of the same loss without having to compile the loss,
albiet there is a small speed penalty for JIT numba compile.
Note:
Requires the pytorch bindings to be installed prior to calling this class.
Warning:
In the case that GPU memory is exhausted in order to compute RNNTLoss, it might cause
a core dump at the cuda level with the following error message.
```
...
costs = costs.to(acts.device)
RuntimeError: CUDA error: an illegal memory access was encountered
terminate called after throwing an instance of 'c10::Error'
```
Please kill all remaining python processes after this point, and use a smaller batch size
for train, validation and test sets so that CUDA memory is not exhausted.
Args:
num_classes: Number of target classes for the joint network to predict.
(Excluding the RNN-T blank token).
reduction: Type of reduction to perform on loss. Possibly values are `mean`, `sum` or None.
None will return a torch vector comprising the individual loss values of the batch.
loss_name: String that is resolved into an RNNT loss function. Available list of losses
is ininitialized in `RNNT_LOSS_RESOLVER` dictionary.
loss_kwargs: Optional Dict of (str, value) pairs that are passed to the instantiated loss
function.
"""
super(RNNTLoss, self).__init__()
if reduction not in [None, 'mean', 'sum', 'mean_batch']:
raise ValueError('`reduction` must be one of [mean, sum, mean_batch]')
self._blank = num_classes
self.reduction = reduction
self._loss = resolve_rnnt_loss(loss_name, blank_idx=self._blank, loss_kwargs=loss_kwargs)
@typecheck()
def forward(self, log_probs, targets, input_lengths, target_lengths):
# Cast to int 32
targets = targets.int()
input_lengths = input_lengths.int()
target_lengths = target_lengths.int()
max_logit_len = input_lengths.max()
max_targets_len = target_lengths.max()
# Force cast joint to float32
# TODO: Remove once Numba supports FP16
if log_probs.dtype != torch.float32:
logits_orig = log_probs
log_probs = log_probs.float()
del logits_orig # save memory *before* computing the loss
# Ensure that shape mismatch does not occur due to padding
# Due to padding and subsequent downsampling, it may be possible that
# max sequence length computed does not match the actual max sequence length
# of the log_probs tensor, therefore we increment the input_lengths by the difference.
# This difference is generally small.
if log_probs.shape[1] != max_logit_len:
log_probs = log_probs.narrow(dim=1, start=0, length=max_logit_len).contiguous()
# Reduce transcript length to correct alignment if additional padding was applied.
# Transcript: [B, L] -> [B, L']; If L' < L
if targets.shape[1] != max_targets_len:
targets = targets.narrow(dim=1, start=0, length=max_targets_len)
# Loss reduction can be dynamic, so set it prior to call
if self.reduction != 'mean_batch':
self._loss.reduction = self.reduction
# Compute RNNT loss
loss = self._loss(acts=log_probs, labels=targets, act_lens=input_lengths, label_lens=target_lengths)
# Loss reduction can be dynamic, so reset it after call
if self.reduction != 'mean_batch':
self._loss.reduction = 'none'
# Loss reduction only for mean_batch mode
if self.reduction == 'mean_batch':
loss = torch.mean(loss)
# del new variables that may have been created
del (
log_probs,
targets,
input_lengths,
target_lengths,
)
return loss
@@ -0,0 +1,118 @@
# Copyright (c) 2020, 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.
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import torch
import torch.nn.functional as F
from nemo.core import Loss, typecheck
from nemo.core.neural_types import EncodedRepresentation, LossType, NeuralType
class Wav2VecLoss(Loss):
@property
def input_types(self):
"""Input types definitions for Wav2VecLoss.
"""
return {
"logits": NeuralType(('B', 'T', 'D'), EncodedRepresentation()),
"targets": NeuralType(('B', 'T', 'D'), EncodedRepresentation()),
"negatives": NeuralType(('N', 'B', 'T', 'D'), EncodedRepresentation()),
"prob_ppl_loss": NeuralType(elements_type=LossType()),
"feature_loss": NeuralType(elements_type=LossType()),
}
@property
def output_types(self):
"""Output types definitions for Wav2VecLoss.
"""
return {
"loss": NeuralType(elements_type=LossType()),
"feature_loss": NeuralType(elements_type=LossType()),
"prob_ppl_loss": NeuralType(elements_type=LossType()),
}
def __init__(self, feature_loss_weight: float, prob_ppl_weight: float, logit_temp: float, reduce: bool = True):
"""
Compute the contrastive loss with respect to the model outputs and sampled negatives from quantizer codebooks.
Args:
feature_loss_weight: Feature penalty weight (L2 Norm)
prob_ppl_weight: Perplexity Loss with respect to probabilities during quantization
logit_temp: Temperature normalization applied in loss.
reduce: Reduce loss via sum reduction (Default true)
"""
super().__init__()
self.feature_loss_weight = feature_loss_weight
self.prob_ppl_weight = prob_ppl_weight
self.logit_temp = logit_temp
self.reduce = reduce
@typecheck()
def forward(
self,
logits: torch.tensor,
targets: torch.tensor,
negatives: torch.tensor,
prob_ppl_loss: torch.tensor,
feature_loss: torch.tensor,
) -> [torch.tensor, torch.tensor, torch.tensor]:
"""
Args:
logits: Model activations
targets: The true target quantized representations
negatives: Sampled negatives from the quantizer codebooks. Sampled from all other timesteps.
feature_loss: Feature penalty (L2 Norm)
prob_ppl_loss: Perplexity Loss with respect to probs in quantization
Returns:
output loss values, feature loss, prob_ppl loss (after scaling).
"""
# Calculate similarity between logits and all targets, returning FxBxT
similarity_scores = self._calculate_similarity(logits, negatives, targets)
# Create targets of size B*T
similarity_targets = logits.new_zeros(similarity_scores.size(1) * similarity_scores.size(2), dtype=torch.long)
# Transpose similarity scores to (T*B)xF for loss
similarity_scores = similarity_scores.transpose(0, 2)
similarity_scores = similarity_scores.reshape(-1, similarity_scores.size(-1))
loss = F.cross_entropy(similarity_scores, similarity_targets, reduction="sum" if self.reduce else "none")
sample_size = similarity_targets.numel()
if self.prob_ppl_weight != 0:
prob_ppl_loss = self.prob_ppl_weight * prob_ppl_loss * sample_size
loss += prob_ppl_loss
if self.feature_loss_weight != 0:
feature_loss = self.feature_loss_weight * feature_loss * sample_size
loss += feature_loss
return loss, feature_loss, prob_ppl_loss
def _calculate_similarity(self, logits, negatives, targets):
neg_is_pos = (targets == negatives).all(-1)
targets = targets.unsqueeze(0)
targets = torch.cat([targets, negatives], dim=0)
logits = torch.cosine_similarity(logits.float(), targets.float(), dim=-1).type_as(logits)
logits /= self.logit_temp
if neg_is_pos.any():
logits[1:][neg_is_pos] = float("-inf")
return logits
@@ -0,0 +1,13 @@
# Copyright (c) 2020, 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.
@@ -0,0 +1,478 @@
# Copyright (c) 2020, 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.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional, Union
import editdistance
import torch
from torchmetrics import Metric
from nemo.collections.asr.parts.submodules import rnnt_beam_decoding as beam_decode
from nemo.collections.asr.parts.submodules import rnnt_greedy_decoding as greedy_decode
from nemo.collections.asr.parts.utils.rnnt_utils import Hypothesis, NBestHypotheses
from nemo.utils import logging
__all__ = ['RNNTDecoding', 'RNNTWER']
class AbstractRNNTDecoding(ABC):
"""
Used for performing RNN-T auto-regressive decoding of the Decoder+Joint network given the encoder state.
Args:
decoding_cfg: A dict-like object which contains the following key-value pairs.
strategy: str value which represents the type of decoding that can occur.
Possible values are :
- greedy, greedy_batch (for greedy decoding).
- beam, tsd, alsd (for beam search decoding).
compute_hypothesis_token_set: A bool flag, which determines whether to compute a list of decoded
tokens as well as the decoded string. Default is False in order to avoid double decoding
unless required.
preserve_alignments: Bool flag which preserves the history of logprobs generated during
decoding (sample / batched). When set to true, the Hypothesis will contain
the non-null value for `logprobs` in it. Here, `logprobs` is a List of torch.Tensors.
In order to obtain this hypothesis, please utilize `rnnt_decoder_predictions_tensor` function
with the `return_hypotheses` flag set to True.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
The config may further contain the following sub-dictionaries:
"greedy":
max_symbols: int, describing the maximum number of target tokens to decode per
timestep during greedy decoding. Setting to larger values allows longer sentences
to be decoded, at the cost of increased execution time.
"beam":
beam_size: int, defining the beam size for beam search. Must be >= 1.
If beam_size == 1, will perform cached greedy search. This might be slightly different
results compared to the greedy search above.
score_norm: optional bool, whether to normalize the returned beam score in the hypotheses.
Set to True by default.
return_best_hypothesis: optional bool, whether to return just the best hypothesis or all of the
hypotheses after beam search has concluded. This flag is set by default.
tsd_max_sym_exp: optional int, determines number of symmetric expansions of the target symbols
per timestep of the acoustic model. Larger values will allow longer sentences to be decoded,
at increased cost to execution time.
alsd_max_target_len: optional int or float, determines the potential maximum target sequence length.
If an integer is provided, it can decode sequences of that particular maximum length.
If a float is provided, it can decode sequences of int(alsd_max_target_len * seq_len),
where seq_len is the length of the acoustic model output (T).
NOTE:
If a float is provided, it can be greater than 1!
By default, a float of 2.0 is used so that a target sequence can be at most twice
as long as the acoustic model output length T.
decoder: The Decoder/Prediction network module.
joint: The Joint network module.
blank_id: The id of the RNNT blank token.
"""
def __init__(self, decoding_cfg, decoder, joint, blank_id: int):
super(AbstractRNNTDecoding, self).__init__()
self.cfg = decoding_cfg
self.blank_id = blank_id
self.compute_hypothesis_token_set = self.cfg.get("compute_hypothesis_token_set", False)
self.preserve_alignments = self.cfg.get('preserve_alignments', False)
possible_strategies = ['greedy', 'greedy_batch', 'beam', 'tsd', 'alsd']
if self.cfg.strategy not in possible_strategies:
raise ValueError(f"Decoding strategy must be one of {possible_strategies}")
if self.cfg.strategy == 'greedy':
self.decoding = greedy_decode.GreedyRNNTInfer(
decoder_model=decoder,
joint_model=joint,
blank_index=self.blank_id,
max_symbols_per_step=self.cfg.greedy.get('max_symbols', None),
preserve_alignments=self.preserve_alignments,
)
elif self.cfg.strategy == 'greedy_batch':
self.decoding = greedy_decode.GreedyBatchedRNNTInfer(
decoder_model=decoder,
joint_model=joint,
blank_index=self.blank_id,
max_symbols_per_step=self.cfg.greedy.get('max_symbols', None),
preserve_alignments=self.preserve_alignments,
)
elif self.cfg.strategy == 'beam':
self.decoding = beam_decode.BeamRNNTInfer(
decoder_model=decoder,
joint_model=joint,
beam_size=self.cfg.beam.beam_size,
return_best_hypothesis=decoding_cfg.beam.get('return_best_hypothesis', True),
search_type='default',
score_norm=self.cfg.beam.get('score_norm', True),
preserve_alignments=self.preserve_alignments,
)
elif self.cfg.strategy == 'tsd':
self.decoding = beam_decode.BeamRNNTInfer(
decoder_model=decoder,
joint_model=joint,
beam_size=self.cfg.beam.beam_size,
return_best_hypothesis=decoding_cfg.beam.get('return_best_hypothesis', True),
search_type='tsd',
score_norm=self.cfg.beam.get('score_norm', True),
tsd_max_sym_exp_per_step=self.cfg.beam.get('tsd_max_sym_exp', 50),
preserve_alignments=self.preserve_alignments,
)
elif self.cfg.strategy == 'alsd':
self.decoding = beam_decode.BeamRNNTInfer(
decoder_model=decoder,
joint_model=joint,
beam_size=self.cfg.beam.beam_size,
return_best_hypothesis=decoding_cfg.beam.get('return_best_hypothesis', True),
search_type='alsd',
score_norm=self.cfg.beam.get('score_norm', True),
alsd_max_target_len=self.cfg.beam.get('alsd_max_target_len', 2),
preserve_alignments=self.preserve_alignments,
)
def rnnt_decoder_predictions_tensor(
self, encoder_output: torch.Tensor, encoded_lengths: torch.Tensor, return_hypotheses: bool = False
) -> (List[str], Optional[List[List[str]]], Optional[Union[Hypothesis, NBestHypotheses]]):
"""
Decode an encoder output by autoregressive decoding of the Decoder+Joint networks.
Args:
encoder_output: torch.Tensor of shape [B, D, T].
encoded_lengths: torch.Tensor containing lengths of the padded encoder outputs. Shape [B].
return_hypotheses: bool. If set to True it will return list of Hypothesis or NBestHypotheses
Returns:
If `return_best_hypothesis` is set:
A tuple (hypotheses, None):
hypotheses - list of Hypothesis (best hypothesis per sample).
Look at rnnt_utils.Hypothesis for more information.
If `return_best_hypothesis` is not set:
A tuple(hypotheses, all_hypotheses)
hypotheses - list of Hypothesis (best hypothesis per sample).
Look at rnnt_utils.Hypothesis for more information.
all_hypotheses - list of NBestHypotheses. Each NBestHypotheses further contains a sorted
list of all the hypotheses of the model per sample.
Look at rnnt_utils.NBestHypotheses for more information.
"""
# Compute hypotheses
with torch.no_grad():
hypotheses_list = self.decoding(
encoder_output=encoder_output, encoded_lengths=encoded_lengths
) # type: [List[Hypothesis]]
# extract the hypotheses
hypotheses_list = hypotheses_list[0] # type: List[Hypothesis]
prediction_list = hypotheses_list
if isinstance(prediction_list[0], NBestHypotheses):
hypotheses = []
all_hypotheses = []
for nbest_hyp in prediction_list: # type: NBestHypotheses
n_hyps = nbest_hyp.n_best_hypotheses # Extract all hypotheses for this sample
decoded_hyps = self.decode_hypothesis(n_hyps) # type: List[str]
hypotheses.append(decoded_hyps[0]) # best hypothesis
all_hypotheses.append(decoded_hyps)
if return_hypotheses:
return hypotheses, all_hypotheses
best_hyp_text = [h.text for h in hypotheses]
all_hyp_text = [h.text for hh in all_hypotheses for h in hh]
return best_hyp_text, all_hyp_text
else:
hypotheses = self.decode_hypothesis(prediction_list) # type: List[str]
if return_hypotheses:
return hypotheses, None
best_hyp_text = [h.text for h in hypotheses]
return best_hyp_text, None
def decode_hypothesis(self, hypotheses_list: List[Hypothesis]) -> List[Union[Hypothesis, NBestHypotheses]]:
"""
Decode a list of hypotheses into a list of strings.
Args:
hypotheses_list: List of Hypothesis.
Returns:
A list of strings.
"""
for ind in range(len(hypotheses_list)):
# Extract the integer encoded hypothesis
prediction = hypotheses_list[ind].y_sequence
if type(prediction) != list:
prediction = prediction.tolist()
# RNN-T sample level is already preprocessed by implicit CTC decoding
# Simply remove any blank tokens
prediction = [p for p in prediction if p != self.blank_id]
# De-tokenize the integer tokens
hypothesis = self.decode_tokens_to_str(prediction)
hypotheses_list[ind].text = hypothesis
if self.compute_hypothesis_token_set:
hypotheses_list[ind].tokens = self.decode_ids_to_tokens(prediction)
return hypotheses_list
@abstractmethod
def decode_tokens_to_str(self, tokens: List[int]) -> str:
"""
Implemented by subclass in order to decoder a token id list into a string.
Args:
tokens: List of int representing the token ids.
Returns:
A decoded string.
"""
raise NotImplementedError()
@abstractmethod
def decode_ids_to_tokens(self, tokens: List[int]) -> List[str]:
"""
Implemented by subclass in order to decode a token id list into a token list.
A token list is the string representation of each token id.
Args:
tokens: List of int representing the token ids.
Returns:
A list of decoded tokens.
"""
raise NotImplementedError()
class RNNTDecoding(AbstractRNNTDecoding):
"""
Used for performing RNN-T auto-regressive decoding of the Decoder+Joint network given the encoder state.
Args:
decoding_cfg: A dict-like object which contains the following key-value pairs.
strategy: str value which represents the type of decoding that can occur.
Possible values are :
- greedy, greedy_batch (for greedy decoding).
- beam, tsd, alsd (for beam search decoding).
compute_hypothesis_token_set: A bool flag, which determines whether to compute a list of decoded
tokens as well as the decoded string. Default is False in order to avoid double decoding
unless required.
preserve_alignments: Bool flag which preserves the history of logprobs generated during
decoding (sample / batched). When set to true, the Hypothesis will contain
the non-null value for `logprobs` in it. Here, `logprobs` is a List of torch.Tensors.
In order to obtain this hypothesis, please utilize `rnnt_decoder_predictions_tensor` function
with the `return_hypotheses` flag set to True.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
The config may further contain the following sub-dictionaries:
"greedy":
max_symbols: int, describing the maximum number of target tokens to decode per
timestep during greedy decoding. Setting to larger values allows longer sentences
to be decoded, at the cost of increased execution time.
"beam":
beam_size: int, defining the beam size for beam search. Must be >= 1.
If beam_size == 1, will perform cached greedy search. This might be slightly different
results compared to the greedy search above.
score_norm: optional bool, whether to normalize the returned beam score in the hypotheses.
Set to True by default.
return_best_hypothesis: optional bool, whether to return just the best hypothesis or all of the
hypotheses after beam search has concluded. This flag is set by default.
tsd_max_sym_exp: optional int, determines number of symmetric expansions of the target symbols
per timestep of the acoustic model. Larger values will allow longer sentences to be decoded,
at increased cost to execution time.
alsd_max_target_len: optional int or float, determines the potential maximum target sequence length.
If an integer is provided, it can decode sequences of that particular maximum length.
If a float is provided, it can decode sequences of int(alsd_max_target_len * seq_len),
where seq_len is the length of the acoustic model output (T).
NOTE:
If a float is provided, it can be greater than 1!
By default, a float of 2.0 is used so that a target sequence can be at most twice
as long as the acoustic model output length T.
decoder: The Decoder/Prediction network module.
joint: The Joint network module.
vocabulary: The vocabulary (excluding the RNNT blank token) which will be used for decoding.
"""
def __init__(
self, decoding_cfg, decoder, joint, vocabulary,
):
blank_id = len(vocabulary)
self.labels_map = dict([(i, vocabulary[i]) for i in range(len(vocabulary))])
super(RNNTDecoding, self).__init__(decoding_cfg=decoding_cfg, decoder=decoder, joint=joint, blank_id=blank_id)
def decode_tokens_to_str(self, tokens: List[int]) -> str:
"""
Implemented by subclass in order to decoder a token list into a string.
Args:
tokens: List of int representing the token ids.
Returns:
A decoded string.
"""
hypothesis = ''.join(self.decode_ids_to_tokens(tokens))
return hypothesis
def decode_ids_to_tokens(self, tokens: List[int]) -> List[str]:
"""
Implemented by subclass in order to decode a token id list into a token list.
A token list is the string representation of each token id.
Args:
tokens: List of int representing the token ids.
Returns:
A list of decoded tokens.
"""
token_list = [self.labels_map[c] for c in tokens if c != self.blank_id]
return token_list
class RNNTWER(Metric):
"""
This metric computes numerator and denominator for Overall Word Error Rate (WER) between prediction and reference texts.
When doing distributed training/evaluation the result of res=WER(predictions, targets, target_lengths) calls
will be all-reduced between all workers using SUM operations.
Here contains two numbers res=[wer_numerator, wer_denominator]. WER=wer_numerator/wer_denominator.
If used with PytorchLightning LightningModule, include wer_numerator and wer_denominators inside validation_step results.
Then aggregate (sum) then at the end of validation epoch to correctly compute validation WER.
Example:
def validation_step(self, batch, batch_idx):
...
wer_num, wer_denom = self.__wer(predictions, transcript, transcript_len)
return {'val_loss': loss_value, 'val_wer_num': wer_num, 'val_wer_denom': wer_denom}
def validation_epoch_end(self, outputs):
...
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 = {'validation_loss': val_loss_mean, 'validation_avg_wer': wer_num / wer_denom}
return {'val_loss': val_loss_mean, 'log': tensorboard_logs}
Args:
decoding: RNNTDecoding object that will perform autoregressive decoding of the RNNT model.
batch_dim_index: Index of the batch dimension.
use_cer: Whether to use Character Error Rate isntead of Word Error Rate.
log_prediction: Whether to log a single decoded sample per call.
Returns:
res: a torch.Tensor object with two elements: [wer_numerator, wer_denominator]. To correctly compute average
text word error rate, compute wer=wer_numerator/wer_denominator
"""
def __init__(
self, decoding: RNNTDecoding, batch_dim_index=0, use_cer=False, log_prediction=True, dist_sync_on_step=False
):
super(RNNTWER, self).__init__(dist_sync_on_step=dist_sync_on_step, compute_on_step=False)
self.decoding = decoding
self.batch_dim_index = batch_dim_index
self.use_cer = use_cer
self.log_prediction = log_prediction
self.blank_id = self.decoding.blank_id
self.labels_map = self.decoding.labels_map
self.add_state("scores", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
self.add_state("words", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
def update(
self,
encoder_output: torch.Tensor,
encoded_lengths: torch.Tensor,
targets: torch.Tensor,
target_lengths: torch.Tensor,
) -> torch.Tensor:
words = 0.0
scores = 0.0
references = []
with torch.no_grad():
# prediction_cpu_tensor = tensors[0].long().cpu()
targets_cpu_tensor = targets.long().cpu()
tgt_lenths_cpu_tensor = target_lengths.long().cpu()
# iterate over batch
for ind in range(targets_cpu_tensor.shape[self.batch_dim_index]):
tgt_len = tgt_lenths_cpu_tensor[ind].item()
target = targets_cpu_tensor[ind][:tgt_len].numpy().tolist()
reference = self.decoding.decode_tokens_to_str(target)
references.append(reference)
hypotheses, _ = self.decoding.rnnt_decoder_predictions_tensor(encoder_output, encoded_lengths)
if self.log_prediction:
logging.info(f"\n")
logging.info(f"reference :{references[0]}")
logging.info(f"predicted :{hypotheses[0]}")
for h, r in zip(hypotheses, references):
if self.use_cer:
h_list = list(h)
r_list = list(r)
else:
h_list = h.split()
r_list = r.split()
words += len(r_list)
# Compute Levenshtein's distance
scores += editdistance.eval(h_list, r_list)
self.scores += torch.tensor(scores, device=self.scores.device, dtype=self.scores.dtype)
self.words += torch.tensor(words, device=self.words.device, dtype=self.words.dtype)
# return torch.tensor([scores, words]).to(predictions.device)
def compute(self):
wer = self.scores.float() / self.words
return wer, self.scores.detach(), self.words.detach()
@dataclass
class RNNTDecodingConfig:
strategy: str = "greedy_batch"
compute_hypothesis_token_set: bool = False
# greedy decoding config
greedy: greedy_decode.GreedyRNNTInferConfig = greedy_decode.GreedyRNNTInferConfig()
# beam decoding config
beam: beam_decode.BeamRNNTInferConfig = beam_decode.BeamRNNTInferConfig(beam_size=4)
@@ -0,0 +1,242 @@
# Copyright (c) 2020, 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.
from dataclasses import dataclass
from typing import List
import editdistance
import torch
from torchmetrics import Metric
from nemo.collections.asr.metrics.rnnt_wer import AbstractRNNTDecoding
from nemo.collections.asr.parts.submodules import rnnt_beam_decoding as beam_decode
from nemo.collections.asr.parts.submodules import rnnt_greedy_decoding as greedy_decode
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.utils import logging
__all__ = ['RNNTBPEDecoding', 'RNNTBPEWER']
class RNNTBPEDecoding(AbstractRNNTDecoding):
"""
Used for performing RNN-T auto-regressive decoding of the Decoder+Joint network given the encoder state.
Args:
decoding_cfg: A dict-like object which contains the following key-value pairs.
strategy: str value which represents the type of decoding that can occur.
Possible values are :
- greedy, greedy_batch (for greedy decoding).
- beam, tsd, alsd (for beam search decoding).
compute_hypothesis_token_set: A bool flag, which determines whether to compute a list of decoded
tokens as well as the decoded string. Default is False in order to avoid double decoding
unless required.
preserve_alignments: Bool flag which preserves the history of logprobs generated during
decoding (sample / batched). When set to true, the Hypothesis will contain
the non-null value for `logprobs` in it. Here, `logprobs` is a List of torch.Tensors.
In order to obtain this hypothesis, please utilize `rnnt_decoder_predictions_tensor` function
with the `return_hypotheses` flag set to True.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
The config may further contain the following sub-dictionaries:
"greedy":
max_symbols: int, describing the maximum number of target tokens to decode per
timestep during greedy decoding. Setting to larger values allows longer sentences
to be decoded, at the cost of increased execution time.
"beam":
beam_size: int, defining the beam size for beam search. Must be >= 1.
If beam_size == 1, will perform cached greedy search. This might be slightly different
results compared to the greedy search above.
score_norm: optional bool, whether to normalize the returned beam score in the hypotheses.
Set to True by default.
return_best_hypothesis: optional bool, whether to return just the best hypothesis or all of the
hypotheses after beam search has concluded.
tsd_max_sym_exp: optional int, determines number of symmetric expansions of the target symbols
per timestep of the acoustic model. Larger values will allow longer sentences to be decoded,
at increased cost to execution time.
alsd_max_target_len: optional int or float, determines the potential maximum target sequence length.
If an integer is provided, it can decode sequences of that particular maximum length.
If a float is provided, it can decode sequences of int(alsd_max_target_len * seq_len),
where seq_len is the length of the acoustic model output (T).
NOTE:
If a float is provided, it can be greater than 1!
By default, a float of 2.0 is used so that a target sequence can be at most twice
as long as the acoustic model output length T.
decoder: The Decoder/Prediction network module.
joint: The Joint network module.
tokenizer: The tokenizer which will be used for decoding.
"""
def __init__(self, decoding_cfg, decoder, joint, tokenizer: TokenizerSpec):
blank_id = tokenizer.tokenizer.vocab_size
self.tokenizer = tokenizer
super(RNNTBPEDecoding, self).__init__(
decoding_cfg=decoding_cfg, decoder=decoder, joint=joint, blank_id=blank_id
)
def decode_tokens_to_str(self, tokens: List[int]) -> str:
"""
Implemented by subclass in order to decoder a token list into a string.
Args:
tokens: List of int representing the token ids.
Returns:
A decoded string.
"""
hypothesis = self.tokenizer.ids_to_text(tokens)
return hypothesis
def decode_ids_to_tokens(self, tokens: List[int]) -> List[str]:
"""
Implemented by subclass in order to decode a token id list into a token list.
A token list is the string representation of each token id.
Args:
tokens: List of int representing the token ids.
Returns:
A list of decoded tokens.
"""
token_list = self.tokenizer.ids_to_tokens(tokens)
return token_list
class RNNTBPEWER(Metric):
"""
This metric computes numerator and denominator for Overall Word Error Rate (WER) between prediction and reference texts.
When doing distributed training/evaluation the result of res=WER(predictions, targets, target_lengths) calls
will be all-reduced between all workers using SUM operations.
Here contains two numbers res=[wer_numerator, wer_denominator]. WER=wer_numerator/wer_denominator.
If used with PytorchLightning LightningModule, include wer_numerator and wer_denominators inside validation_step results.
Then aggregate (sum) then at the end of validation epoch to correctly compute validation WER.
Example:
def validation_step(self, batch, batch_idx):
...
wer_num, wer_denom = self.__wer(predictions, transcript, transcript_len)
return {'val_loss': loss_value, 'val_wer_num': wer_num, 'val_wer_denom': wer_denom}
def validation_epoch_end(self, outputs):
...
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 = {'validation_loss': val_loss_mean, 'validation_avg_wer': wer_num / wer_denom}
return {'val_loss': val_loss_mean, 'log': tensorboard_logs}
Args:
decoding: RNNTBPEDecoding object that will perform autoregressive decoding of the RNNT model.
batch_dim_index: Index of the batch dimension.
use_cer: Whether to use Character Error Rate isntead of Word Error Rate.
log_prediction: Whether to log a single decoded sample per call.
Returns:
res: a torch.Tensor object with two elements: [wer_numerator, wer_denominator]. To correctly compute average
text word error rate, compute wer=wer_numerator/wer_denominator
"""
def __init__(
self,
decoding: RNNTBPEDecoding,
batch_dim_index=0,
use_cer: bool = False,
log_prediction: bool = True,
dist_sync_on_step=False,
):
super(RNNTBPEWER, self).__init__(dist_sync_on_step=dist_sync_on_step, compute_on_step=False)
self.decoding = decoding
self.batch_dim_index = batch_dim_index
self.use_cer = use_cer
self.log_prediction = log_prediction
self.blank_id = self.decoding.blank_id
self.tokenizer = self.decoding.tokenizer
self.add_state("scores", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
self.add_state("words", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
def update(
self,
encoder_output: torch.Tensor,
encoded_lengths: torch.Tensor,
targets: torch.Tensor,
target_lengths: torch.Tensor,
) -> torch.Tensor:
words = 0.0
scores = 0.0
references = []
with torch.no_grad():
# prediction_cpu_tensor = tensors[0].long().cpu()
targets_cpu_tensor = targets.long().cpu()
tgt_lenths_cpu_tensor = target_lengths.long().cpu()
# iterate over batch
for ind in range(targets_cpu_tensor.shape[self.batch_dim_index]):
tgt_len = tgt_lenths_cpu_tensor[ind].item()
target = targets_cpu_tensor[ind][:tgt_len].numpy().tolist()
reference = self.decoding.decode_tokens_to_str(target)
references.append(reference)
hypotheses, _ = self.decoding.rnnt_decoder_predictions_tensor(encoder_output, encoded_lengths)
if self.log_prediction:
logging.info(f"\n")
logging.info(f"reference :{references[0]}")
logging.info(f"predicted :{hypotheses[0]}")
for h, r in zip(hypotheses, references):
if self.use_cer:
h_list = list(h)
r_list = list(r)
else:
h_list = h.split()
r_list = r.split()
words += len(r_list)
# Compute Levenshtein's distance
scores += editdistance.eval(h_list, r_list)
del hypotheses
self.scores += torch.tensor(scores, device=self.scores.device, dtype=self.scores.dtype)
self.words += torch.tensor(words, device=self.words.device, dtype=self.words.dtype)
# return torch.tensor([scores, words]).to(predictions.device)
def compute(self):
wer = self.scores.float() / self.words
return wer, self.scores.detach(), self.words.detach()
@dataclass
class RNNTBPEDecodingConfig:
strategy: str = "greedy_batch"
compute_hypothesis_token_set: bool = False
# greedy decoding config
greedy: greedy_decode.GreedyRNNTInferConfig = greedy_decode.GreedyRNNTInferConfig()
# beam decoding config
beam: beam_decode.BeamRNNTInferConfig = beam_decode.BeamRNNTInferConfig(beam_size=4)
+247
View File
@@ -0,0 +1,247 @@
# Copyright (c) 2020, 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.
from typing import List
import editdistance
import torch
from torchmetrics import Metric
from nemo.collections.asr.parts.utils.rnnt_utils import Hypothesis
from nemo.utils import logging
__all__ = ['word_error_rate', 'WER']
def word_error_rate(hypotheses: List[str], references: List[str], use_cer=False) -> float:
"""
Computes Average Word Error rate between two texts represented as
corresponding lists of string. Hypotheses and references must have same
length.
Args:
hypotheses: list of hypotheses
references: list of references
use_cer: bool, set True to enable cer
Returns:
(float) average word error rate
"""
scores = 0
words = 0
if len(hypotheses) != len(references):
raise ValueError(
"In word error rate calculation, hypotheses and reference"
" lists must have the same number of elements. But I got:"
"{0} and {1} correspondingly".format(len(hypotheses), len(references))
)
for h, r in zip(hypotheses, references):
if use_cer:
h_list = list(h)
r_list = list(r)
else:
h_list = h.split()
r_list = r.split()
words += len(r_list)
scores += editdistance.eval(h_list, r_list)
if words != 0:
wer = 1.0 * scores / words
else:
wer = float('inf')
return wer
class WER(Metric):
"""
This metric computes numerator and denominator for Overall Word Error Rate (WER) between prediction and reference texts.
When doing distributed training/evaluation the result of res=WER(predictions, targets, target_lengths) calls
will be all-reduced between all workers using SUM operations.
Here contains two numbers res=[wer_numerator, wer_denominator]. WER=wer_numerator/wer_denominator.
If used with PytorchLightning LightningModule, include wer_numerator and wer_denominators inside validation_step results.
Then aggregate (sum) then at the end of validation epoch to correctly compute validation WER.
Example:
def validation_step(self, batch, batch_idx):
...
wer_num, wer_denom = self.__wer(predictions, transcript, transcript_len)
return {'val_loss': loss_value, 'val_wer_num': wer_num, 'val_wer_denom': wer_denom}
def validation_epoch_end(self, outputs):
...
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 = {'validation_loss': val_loss_mean, 'validation_avg_wer': wer_num / wer_denom}
return {'val_loss': val_loss_mean, 'log': tensorboard_logs}
Args:
vocabulary: List of strings that describes the vocabulary of the dataset.
batch_dim_index: Index of the batch dimension.
use_cer: Whether to use Character Error Rate instead of Word Error Rate.
ctc_decode: Whether to use CTC decoding or not. Currently, must be set.
log_prediction: Whether to log a single decoded sample per call.
Returns:
res: a torch.Tensor object with two elements: [wer_numerator, wer_denominator]. To correctly compute average
text word error rate, compute wer=wer_numerator/wer_denominator
"""
def __init__(
self,
vocabulary,
batch_dim_index=0,
use_cer=False,
ctc_decode=True,
log_prediction=True,
dist_sync_on_step=False,
):
super().__init__(dist_sync_on_step=dist_sync_on_step, compute_on_step=False)
self.batch_dim_index = batch_dim_index
self.blank_id = len(vocabulary)
self.labels_map = dict([(i, vocabulary[i]) for i in range(len(vocabulary))])
self.use_cer = use_cer
self.ctc_decode = ctc_decode
self.log_prediction = log_prediction
self.add_state("scores", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
self.add_state("words", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
def ctc_decoder_predictions_tensor(
self, predictions: torch.Tensor, predictions_len: torch.Tensor = None, return_hypotheses: bool = False,
) -> List[str]:
"""
Decodes a sequence of labels to words
Args:
predictions: A torch.Tensor of shape [Batch, Time] of integer indices that correspond
to the index of some character in the label set.
predictions_len: Optional tensor of length `Batch` which contains the integer lengths
of the sequence in the padded `predictions` tensor.
return_hypotheses: Bool flag whether to return just the decoding predictions of the model
or a Hypothesis object that holds information such as the decoded `text`,
the `alignment` of emited by the CTC Model, and the `length` of the sequence (if available).
May also contain the log-probabilities of the decoder (if this method is called via
transcribe())
Returns:
Either a list of str which represent the CTC decoded strings per sample,
or a list of Hypothesis objects containing additional information.
"""
hypotheses = []
# Drop predictions to CPU
prediction_cpu_tensor = predictions.long().cpu()
# iterate over batch
for ind in range(prediction_cpu_tensor.shape[self.batch_dim_index]):
prediction = prediction_cpu_tensor[ind].detach().numpy().tolist()
if predictions_len is not None:
prediction = prediction[: predictions_len[ind]]
# CTC decoding procedure
decoded_prediction = []
previous = self.blank_id
for p in prediction:
if (p != previous or previous == self.blank_id) and p != self.blank_id:
decoded_prediction.append(p)
previous = p
text = self.decode_tokens_to_str(decoded_prediction)
if not return_hypotheses:
hypothesis = text
else:
hypothesis = Hypothesis(
y_sequence=None,
score=-1.0,
text=text,
alignments=prediction,
length=predictions_len[ind] if predictions_len is not None else 0,
)
hypotheses.append(hypothesis)
return hypotheses
def decode_tokens_to_str(self, tokens: List[int]) -> str:
"""
Implemented in order to decoder a token list into a string.
Args:
tokens: List of int representing the token ids.
Returns:
A decoded string.
"""
hypothesis = ''.join(self.decode_ids_to_tokens(tokens))
return hypothesis
def decode_ids_to_tokens(self, tokens: List[int]) -> List[str]:
"""
Implemented in order to decode a token id list into a token list.
A token list is the string representation of each token id.
Args:
tokens: List of int representing the token ids.
Returns:
A list of decoded tokens.
"""
token_list = [self.labels_map[c] for c in tokens if c != self.blank_id]
return token_list
def update(
self,
predictions: torch.Tensor,
targets: torch.Tensor,
target_lengths: torch.Tensor,
predictions_lengths: torch.Tensor = None,
) -> torch.Tensor:
words = 0.0
scores = 0.0
references = []
with torch.no_grad():
# prediction_cpu_tensor = tensors[0].long().cpu()
targets_cpu_tensor = targets.long().cpu()
tgt_lenths_cpu_tensor = target_lengths.long().cpu()
# iterate over batch
for ind in range(targets_cpu_tensor.shape[self.batch_dim_index]):
tgt_len = tgt_lenths_cpu_tensor[ind].item()
target = targets_cpu_tensor[ind][:tgt_len].numpy().tolist()
reference = self.decode_tokens_to_str(target)
references.append(reference)
if self.ctc_decode:
hypotheses = self.ctc_decoder_predictions_tensor(predictions, predictions_lengths)
else:
raise NotImplementedError("Implement me if you need non-CTC decode on predictions")
if self.log_prediction:
logging.info(f"\n")
logging.info(f"reference:{references[0]}")
logging.info(f"predicted:{hypotheses[0]}")
for h, r in zip(hypotheses, references):
if self.use_cer:
h_list = list(h)
r_list = list(r)
else:
h_list = h.split()
r_list = r.split()
words += len(r_list)
# Compute Levenstein's distance
scores += editdistance.eval(h_list, r_list)
self.scores = torch.tensor(scores, device=self.scores.device, dtype=self.scores.dtype)
self.words = torch.tensor(words, device=self.words.device, dtype=self.words.dtype)
# return torch.tensor([scores, words]).to(predictions.device)
def compute(self):
scores = self.scores.detach().float()
words = self.words.detach().float()
return scores / words, scores, words
+210
View File
@@ -0,0 +1,210 @@
# Copyright (c) 2020, 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.
from typing import List
import editdistance
import torch
from torchmetrics import Metric
from nemo.collections.asr.parts.utils.rnnt_utils import Hypothesis
from nemo.collections.common.tokenizers.tokenizer_spec import TokenizerSpec
from nemo.utils import logging
class WERBPE(Metric):
"""
This metric computes numerator and denominator for Overall Word Error Rate for BPE tokens (WER-BPE) between prediction and reference texts.
When doing distributed training/evaluation the result of res=WERBPE(predictions, targets, target_lengths) calls
will be all-reduced between all workers using SUM operations.
Here contains two numbers res=[wer_numerator, wer_denominator]. WERBPE=wer_numerator/wer_denominator.
If used with PytorchLightning LightningModule, include wer_numerator and wer_denominators inside validation_step results.
Then aggregate (sum) then at the end of validation epoch to correctly compute validation WER.
Example:
def validation_step(self, batch, batch_idx):
...
wer_num, wer_denom = self.__wer(predictions, transcript, transcript_len)
return {'val_loss': loss_value, 'val_wer_num': wer_num, 'val_wer_denom': wer_denom}
def validation_epoch_end(self, outputs):
...
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 = {'validation_loss': val_loss_mean, 'validation_avg_wer': wer_num / wer_denom}
return {'val_loss': val_loss_mean, 'log': tensorboard_logs}
Args:
vocabulary: NeMo tokenizer object, which inherits from TokenizerSpec.
batch_dim_index: Index of the batch dimension.
use_cer: Whether to compute word-error-rate or character-error-rate.
ctc_decode: Whether to perform CTC decode.
log_prediction: Whether to log a single decoded sample per call.
Returns:
res: a torch.Tensor object with two elements: [wer_numerator, wer_denominators]. To correctly compute average
text word error rate, compute wer=wer_numerator/wer_denominators
"""
def __init__(
self,
tokenizer: TokenizerSpec,
batch_dim_index=0,
use_cer=False,
ctc_decode=True,
log_prediction=True,
dist_sync_on_step=False,
):
super().__init__(dist_sync_on_step=dist_sync_on_step, compute_on_step=False)
self.tokenizer = tokenizer
self.batch_dim_index = batch_dim_index
self.blank_id = tokenizer.tokenizer.vocab_size
self.use_cer = use_cer
self.ctc_decode = ctc_decode
self.log_prediction = log_prediction
self.add_state("scores", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
self.add_state("words", default=torch.tensor(0), dist_reduce_fx='sum', persistent=False)
def ctc_decoder_predictions_tensor(
self, predictions: torch.Tensor, predictions_len: torch.Tensor = None, return_hypotheses: bool = False
) -> List[str]:
"""
Decodes a sequence of labels to words
Args:
predictions: A torch.Tensor of shape [Batch, Time] of integer indices that correspond
to the index of some character in the vocabulary of the tokenizer.
predictions_len: Optional tensor of length `Batch` which contains the integer lengths
of the sequence in the padded `predictions` tensor.
return_hypotheses: Bool flag whether to return just the decoding predictions of the model
or a Hypothesis object that holds information such as the decoded `text`,
the `alignment` of emited by the CTC Model, and the `length` of the sequence (if available).
May also contain the log-probabilities of the decoder (if this method is called via
transcribe()) inside `y_sequence`, otherwise it is set None as it is a duplicate of
`alignments`.
Returns:
Either a list of str which represent the CTC decoded strings per sample,
or a list of Hypothesis objects containing additional information.
"""
hypotheses = []
# Drop predictions to CPU
prediction_cpu_tensor = predictions.long().cpu()
# iterate over batch
for ind in range(prediction_cpu_tensor.shape[self.batch_dim_index]):
prediction = prediction_cpu_tensor[ind].detach().numpy().tolist()
if predictions_len is not None:
prediction = prediction[: predictions_len[ind]]
# CTC decoding procedure
decoded_prediction = []
previous = self.blank_id
for p in prediction:
if (p != previous or previous == self.blank_id) and p != self.blank_id:
decoded_prediction.append(p)
previous = p
text = self.decode_tokens_to_str(decoded_prediction)
if not return_hypotheses:
hypothesis = text
else:
hypothesis = Hypothesis(
y_sequence=None, # logprob info added by transcribe method
score=-1.0,
text=text,
alignments=prediction,
length=predictions_len[ind] if predictions_len is not None else 0,
)
hypotheses.append(hypothesis)
return hypotheses
def decode_tokens_to_str(self, tokens: List[int]) -> str:
"""
Implemented in order to decoder a token list into a string.
Args:
tokens: List of int representing the token ids.
Returns:
A decoded string.
"""
hypothesis = self.tokenizer.ids_to_text(tokens)
return hypothesis
def decode_ids_to_tokens(self, tokens: List[int]) -> List[str]:
"""
Implemented in order to decode a token id list into a token list.
A token list is the string representation of each token id.
Args:
tokens: List of int representing the token ids.
Returns:
A list of decoded tokens.
"""
token_list = self.tokenizer.ids_to_tokens(tokens)
return token_list
def update(
self,
predictions: torch.Tensor,
targets: torch.Tensor,
target_lengths: torch.Tensor,
predictions_lengths: torch.Tensor = None,
):
words = 0.0
scores = 0.0
references = []
with torch.no_grad():
# prediction_cpu_tensor = tensors[0].long().cpu()
targets_cpu_tensor = targets.long().cpu()
tgt_lenths_cpu_tensor = target_lengths.long().cpu()
# iterate over batch
for ind in range(targets_cpu_tensor.shape[self.batch_dim_index]):
tgt_len = tgt_lenths_cpu_tensor[ind].item()
target = targets_cpu_tensor[ind][:tgt_len].numpy().tolist()
reference = self.decode_tokens_to_str(target)
references.append(reference)
if self.ctc_decode:
hypotheses = self.ctc_decoder_predictions_tensor(predictions, predictions_lengths)
else:
raise NotImplementedError("Implement me if you need non-CTC decode on predictions")
if self.log_prediction:
logging.info(f"\n")
logging.info(f"reference:{references[0]}")
logging.info(f"predicted:{hypotheses[0]}")
for h, r in zip(hypotheses, references):
if self.use_cer:
h_list = list(h)
r_list = list(r)
else:
h_list = h.split()
r_list = r.split()
words += len(r_list)
# Compute Levenstein's distance
scores += editdistance.eval(h_list, r_list)
self.scores = torch.tensor(scores, device=self.scores.device, dtype=self.scores.dtype)
self.words = torch.tensor(words, device=self.words.device, dtype=self.words.dtype)
# return torch.tensor([scores, words]).to(predictions.device)
def compute(self):
scores = self.scores.detach().float()
words = self.words.detach().float()
return scores / words, scores, words
@@ -0,0 +1,22 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.models.asr_model import ASRModel
from nemo.collections.asr.models.classification_models import EncDecClassificationModel
from nemo.collections.asr.models.clustering_diarizer import ClusteringDiarizer
from nemo.collections.asr.models.ctc_bpe_models import EncDecCTCModelBPE
from nemo.collections.asr.models.ctc_models import EncDecCTCModel
from nemo.collections.asr.models.label_models import EncDecSpeakerLabelModel, ExtractSpeakerEmbeddingsModel
from nemo.collections.asr.models.rnnt_bpe_models import EncDecRNNTBPEModel
from nemo.collections.asr.models.rnnt_models import EncDecRNNTModel
@@ -0,0 +1,91 @@
# Copyright (c) 2020, 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 abc import ABC, abstractmethod
from typing import List, Optional, Union
import torch
from omegaconf import OmegaConf
from nemo.core.classes import ModelPT
from nemo.core.classes.exportable import Exportable
from nemo.utils import model_utils
__all__ = ['ASRModel']
class ASRModel(ModelPT, ABC):
@abstractmethod
def transcribe(self, paths2audio_files: List[str], batch_size: int = 4) -> List[str]:
"""
Takes paths to audio files and returns text transcription
Args:
paths2audio_files: paths to audio fragment to be transcribed
Returns:
transcription texts
"""
pass
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
val_loss_mean = torch.stack([x['val_loss'] for x in outputs]).mean()
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': val_loss_mean, 'val_wer': wer_num / wer_denom}
return {'val_loss': val_loss_mean, 'log': tensorboard_logs}
def multi_test_epoch_end(self, outputs, dataloader_idx: int = 0):
val_loss_mean = torch.stack([x['test_loss'] for x in outputs]).mean()
wer_num = torch.stack([x['test_wer_num'] for x in outputs]).sum()
wer_denom = torch.stack([x['test_wer_denom'] for x in outputs]).sum()
tensorboard_logs = {'test_loss': val_loss_mean, 'test_wer': wer_num / wer_denom}
return {'test_loss': val_loss_mean, 'log': tensorboard_logs}
@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.
"""
# recursively walk the subclasses to generate pretrained model info
list_of_models = model_utils.resolve_subclass_pretrained_model_info(cls)
return list_of_models
class ExportableEncDecModel(Exportable):
"""
Simple utiliy mix-in to export models that consist of encoder/decoder pair
plus pre/post processor, but have to be exported as encoder/decoder pair only
(covers most ASR classes)
"""
@property
def input_module(self):
return self.encoder
@property
def output_module(self):
return self.decoder
def forward_for_export(self, input, length=None):
encoder_output = self.input_module(input, length)
if isinstance(encoder_output, tuple):
return self.output_module(encoder_output[0])
else:
return self.output_module(encoder_output)
def _prepare_for_export(self, **kwargs):
self.input_module._prepare_for_export(**kwargs)
self.output_module._prepare_for_export(**kwargs)
@@ -0,0 +1,750 @@
# Copyright (c) 2020, 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 copy
import json
import os
import tempfile
from abc import abstractmethod
from math import ceil
from typing import Dict, List, Optional, Union
import torch
from omegaconf import DictConfig, ListConfig, OmegaConf
from pytorch_lightning import Trainer
from torchmetrics.regression import MeanAbsoluteError, MeanSquaredError
from nemo.collections.asr.data import audio_to_label_dataset
from nemo.collections.asr.models.asr_model import ASRModel, ExportableEncDecModel
from nemo.collections.asr.parts.preprocessing.features import WaveformFeaturizer
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.collections.common.losses import CrossEntropyLoss, MSELoss
from nemo.collections.common.metrics import TopKClassificationAccuracy
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import *
from nemo.utils import logging, model_utils
__all__ = ['EncDecClassificationModel', 'EncDecRegressionModel']
class _EncDecBaseModel(ASRModel, ExportableEncDecModel):
"""Encoder decoder Classification models."""
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
# Get global rank and total number of GPU workers for IterableDataset partitioning, if applicable
# Global_rank and local_rank is set by LightningModule in Lightning 1.2.0
self.world_size = 1
if trainer is not None:
self.world_size = trainer.num_nodes * trainer.num_gpus
# Convert config to a DictConfig
cfg = model_utils.convert_model_config_to_dict_config(cfg)
# Convert config to support Hydra 1.0+ instantiation
cfg = model_utils.maybe_update_config_version(cfg)
self.is_regression_task = cfg.get('is_regression_task', False)
# Change labels if needed
self._update_decoder_config(cfg.labels, cfg.decoder)
super().__init__(cfg=cfg, trainer=trainer)
if hasattr(self._cfg, 'spec_augment') and self._cfg.spec_augment is not None:
self.spec_augmentation = ASRModel.from_config_dict(self._cfg.spec_augment)
else:
self.spec_augmentation = None
if hasattr(self._cfg, 'crop_or_pad_augment') and self._cfg.crop_or_pad_augment is not None:
self.crop_or_pad = ASRModel.from_config_dict(self._cfg.crop_or_pad_augment)
else:
self.crop_or_pad = None
self.preprocessor = self._setup_preprocessor()
self.encoder = self._setup_encoder()
self.decoder = self._setup_decoder()
self.loss = self._setup_loss()
self._setup_metrics()
@abstractmethod
def _setup_preprocessor(self):
"""
Setup preprocessor for audio data
Returns: Preprocessor
"""
pass
@abstractmethod
def _setup_encoder(self):
"""
Setup encoder for the Encoder-Decoder network
Returns: Encoder
"""
pass
@abstractmethod
def _setup_decoder(self):
"""
Setup decoder for the Encoder-Decoder network
Returns: Decoder
"""
pass
@abstractmethod
def _setup_loss(self):
"""
Setup loss function for training
Returns: Loss function
"""
pass
@abstractmethod
def _setup_metrics(self):
"""
Setup metrics to be tracked in addition to loss
Returns: void
"""
pass
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
if hasattr(self.preprocessor, '_sample_rate'):
audio_eltype = AudioSignal(freq=self.preprocessor._sample_rate)
else:
audio_eltype = AudioSignal()
return {
"input_signal": NeuralType(('B', 'T'), audio_eltype),
"input_signal_length": NeuralType(tuple('B'), LengthsType()),
}
@property
@abstractmethod
def output_types(self) -> Optional[Dict[str, NeuralType]]:
pass
def forward(self, input_signal, input_signal_length):
processed_signal, processed_signal_len = self.preprocessor(
input_signal=input_signal, length=input_signal_length,
)
# Crop or pad is always applied
if self.crop_or_pad is not None:
processed_signal, processed_signal_len = self.crop_or_pad(
input_signal=processed_signal, length=processed_signal_len
)
# 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_len)
encoded, encoded_len = self.encoder(audio_signal=processed_signal, length=processed_signal_len)
logits = self.decoder(encoder_output=encoded)
return logits
def setup_training_data(self, train_data_config: Optional[Union[DictConfig, Dict]]):
if 'shuffle' not in train_data_config:
train_data_config['shuffle'] = True
# preserve config
self._update_dataset_config(dataset_name='train', config=train_data_config)
self._train_dl = self._setup_dataloader_from_config(config=DictConfig(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 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'])
)
def setup_validation_data(self, val_data_config: Optional[Union[DictConfig, Dict]]):
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=DictConfig(val_data_config))
def setup_test_data(self, test_data_config: Optional[Union[DictConfig, Dict]]):
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=DictConfig(test_data_config))
def test_dataloader(self):
if self._test_dl is not None:
return self._test_dl
def _setup_dataloader_from_config(self, config: DictConfig):
OmegaConf.set_struct(config, False)
config.is_regression_task = self.is_regression_task
OmegaConf.set_struct(config, True)
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
featurizer = WaveformFeaturizer(
sample_rate=config['sample_rate'], int_values=config.get('int_values', False), augmentor=augmentor
)
shuffle = config['shuffle']
# Instantiate tarred dataset loader or normal dataset loader
if config.get('is_tarred', False):
if ('tarred_audio_filepaths' in config and config['tarred_audio_filepaths'] is None) or (
'manifest_filepath' in config and config['manifest_filepath'] is None
):
logging.warning(
"Could not load dataset as `manifest_filepath` is None or "
f"`tarred_audio_filepaths` is None. Provided config : {config}"
)
return None
if 'vad_stream' in config and config['vad_stream']:
logging.warning("VAD inference does not support tarred dataset now")
return None
shuffle_n = config.get('shuffle_n', 4 * config['batch_size']) if shuffle else 0
dataset = audio_to_label_dataset.get_tarred_classification_label_dataset(
featurizer=featurizer,
config=OmegaConf.to_container(config),
shuffle_n=shuffle_n,
global_rank=self.global_rank,
world_size=self.world_size,
)
shuffle = False
batch_size = config['batch_size']
collate_func = dataset.collate_fn
else:
if 'manifest_filepath' in config and config['manifest_filepath'] is None:
logging.warning(f"Could not load dataset as `manifest_filepath` is None. Provided config : {config}")
return None
if 'vad_stream' in config and config['vad_stream']:
logging.info("Perform streaming frame-level VAD")
dataset = audio_to_label_dataset.get_speech_label_dataset(
featurizer=featurizer, config=OmegaConf.to_container(config)
)
batch_size = 1
collate_func = dataset.vad_frame_seq_collate_fn
else:
dataset = audio_to_label_dataset.get_classification_label_dataset(
featurizer=featurizer, config=OmegaConf.to_container(config)
)
batch_size = config['batch_size']
collate_func = dataset.collate_fn
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=batch_size,
collate_fn=collate_func,
drop_last=config.get('drop_last', False),
shuffle=shuffle,
num_workers=config.get('num_workers', 0),
pin_memory=config.get('pin_memory', False),
)
@torch.no_grad()
def transcribe(self, paths2audio_files: List[str], batch_size: int = 4, logprobs=False) -> List[str]:
"""
Generate class labels for provided audio files. Use this method for debugging and prototyping.
Args:
paths2audio_files: (a list) of paths to audio files. \
Recommended length per file is approximately 1 second.
batch_size: (int) batch size to use during inference. \
Bigger will result in better throughput performance but would use more memory.
logprobs: (bool) pass True to get log probabilities instead of class labels.
Returns:
A list of transcriptions (or raw log probabilities if logprobs is True) in the same order as paths2audio_files
"""
if paths2audio_files is None or len(paths2audio_files) == 0:
return []
# We will store transcriptions here
labels = []
# Model's mode and device
mode = self.training
device = next(self.parameters()).device
dither_value = self.preprocessor.featurizer.dither
pad_to_value = self.preprocessor.featurizer.pad_to
try:
self.preprocessor.featurizer.dither = 0.0
self.preprocessor.featurizer.pad_to = 0
# Switch model to evaluation mode
self.eval()
logging_level = logging.get_verbosity()
logging.set_verbosity(logging.WARNING)
# Work in tmp directory - will store manifest file there
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, 'manifest.json'), 'w') as fp:
for audio_file in paths2audio_files:
label = 0.0 if self.is_regression_task else self.cfg.labels[0]
entry = {'audio_filepath': audio_file, 'duration': 100000.0, 'label': label}
fp.write(json.dumps(entry) + '\n')
config = {'paths2audio_files': paths2audio_files, 'batch_size': batch_size, 'temp_dir': tmpdir}
temporary_datalayer = self._setup_transcribe_dataloader(config)
for test_batch in temporary_datalayer:
logits = self.forward(
input_signal=test_batch[0].to(device), input_signal_length=test_batch[1].to(device)
)
if logprobs:
# dump log probs per file
for idx in range(logits.shape[0]):
lg = logits[idx]
labels.append(lg.cpu().numpy())
else:
labels_k = []
top_ks = self._accuracy.top_k
for top_k_i in top_ks:
# replace top k value with current top k
self._accuracy.top_k = top_k_i
labels_k_i = self._accuracy.top_k_predicted_labels(logits)
labels_k.append(labels_k_i)
# convenience: if only one top_k, pop out the nested list
if len(top_ks) == 1:
labels_k = labels_k[0]
labels += labels_k
# reset top k to orignal value
self._accuracy.top_k = top_ks
del test_batch
finally:
# set mode back to its original value
self.train(mode=mode)
self.preprocessor.featurizer.dither = dither_value
self.preprocessor.featurizer.pad_to = pad_to_value
logging.set_verbosity(logging_level)
return labels
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:
Returns:
A pytorch DataLoader for the given audio file(s).
"""
dl_config = {
'manifest_filepath': os.path.join(config['temp_dir'], 'manifest.json'),
'sample_rate': self.preprocessor._sample_rate,
'labels': self.cfg.labels,
'batch_size': min(config['batch_size'], len(config['paths2audio_files'])),
'trim_silence': False,
'shuffle': False,
}
temporary_datalayer = self._setup_dataloader_from_config(config=DictConfig(dl_config))
return temporary_datalayer
@abstractmethod
def _update_decoder_config(self, labels, cfg):
pass
class EncDecClassificationModel(_EncDecBaseModel):
"""Encoder decoder Classification models."""
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
if cfg.get("is_regression_task", False):
raise ValueError(f"EndDecClassificationModel requires the flag is_regression_task to be set as false")
super().__init__(cfg=cfg, trainer=trainer)
def _setup_preprocessor(self):
return EncDecClassificationModel.from_config_dict(self._cfg.preprocessor)
def _setup_encoder(self):
return EncDecClassificationModel.from_config_dict(self._cfg.encoder)
def _setup_decoder(self):
return EncDecClassificationModel.from_config_dict(self._cfg.decoder)
def _setup_loss(self):
return CrossEntropyLoss()
def _setup_metrics(self):
self._accuracy = TopKClassificationAccuracy(dist_sync_on_step=True)
@classmethod
def list_available_models(cls) -> Optional[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.
"""
results = []
model = PretrainedModelInfo(
pretrained_model_name="vad_telephony_marblenet",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:vad_telephony_marblenet",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/vad_telephony_marblenet/versions/1.0.0rc1/files/vad_telephony_marblenet.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="vad_marblenet",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:vad_marblenet",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/vad_marblenet/versions/1.0.0rc1/files/vad_marblenet.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="commandrecognition_en_matchboxnet3x1x64_v1",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:commandrecognition_en_matchboxnet3x1x64_v1",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/commandrecognition_en_matchboxnet3x1x64_v1/versions/1.0.0rc1/files/commandrecognition_en_matchboxnet3x1x64_v1.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="commandrecognition_en_matchboxnet3x2x64_v1",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:commandrecognition_en_matchboxnet3x2x64_v1",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/commandrecognition_en_matchboxnet3x2x64_v1/versions/1.0.0rc1/files/commandrecognition_en_matchboxnet3x2x64_v1.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="commandrecognition_en_matchboxnet3x1x64_v2",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:commandrecognition_en_matchboxnet3x1x64_v2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/commandrecognition_en_matchboxnet3x1x64_v2/versions/1.0.0rc1/files/commandrecognition_en_matchboxnet3x1x64_v2.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="commandrecognition_en_matchboxnet3x2x64_v2",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:commandrecognition_en_matchboxnet3x2x64_v2",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/commandrecognition_en_matchboxnet3x2x64_v2/versions/1.0.0rc1/files/commandrecognition_en_matchboxnet3x2x64_v2.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="commandrecognition_en_matchboxnet3x1x64_v2_subset_task",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:commandrecognition_en_matchboxnet3x1x64_v2_subset_task",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/commandrecognition_en_matchboxnet3x1x64_v2_subset_task/versions/1.0.0rc1/files/commandrecognition_en_matchboxnet3x1x64_v2_subset_task.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="commandrecognition_en_matchboxnet3x2x64_v2_subset_task",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:commandrecognition_en_matchboxnet3x2x64_v2_subset_task",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/commandrecognition_en_matchboxnet3x2x64_v2_subset_task/versions/1.0.0rc1/files/commandrecognition_en_matchboxnet3x2x64_v2_subset_task.nemo",
)
results.append(model)
return results
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"outputs": NeuralType(('B', 'D'), LogitsType())}
# PTL-specific methods
def training_step(self, batch, batch_nb):
self.training_step_end()
audio_signal, audio_signal_len, labels, labels_len = batch
logits = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss_value = self.loss(logits=logits, labels=labels)
self.log('train_loss', loss_value)
self.log('learning_rate', self._optimizer.param_groups[0]['lr'])
self._accuracy(logits=logits, labels=labels)
topk_scores = self._accuracy.compute()
self._accuracy.reset()
for top_k, score in zip(self._accuracy.top_k, topk_scores):
self.log('training_batch_accuracy_top@{}'.format(top_k), score)
return {
'loss': loss_value,
}
def validation_step(self, batch, batch_idx, dataloader_idx=0):
audio_signal, audio_signal_len, labels, labels_len = batch
logits = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss_value = self.loss(logits=logits, labels=labels)
acc = self._accuracy(logits=logits, labels=labels)
correct_counts, total_counts = self._accuracy.correct_counts_k, self._accuracy.total_counts_k
return {
'val_loss': loss_value,
'val_correct_counts': correct_counts,
'val_total_counts': total_counts,
'val_acc': acc,
}
def test_step(self, batch, batch_idx, dataloader_idx=0):
audio_signal, audio_signal_len, labels, labels_len = batch
logits = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss_value = self.loss(logits=logits, labels=labels)
acc = self._accuracy(logits=logits, labels=labels)
correct_counts, total_counts = self._accuracy.correct_counts_k, self._accuracy.total_counts_k
return {
'test_loss': loss_value,
'test_correct_counts': correct_counts,
'test_total_counts': total_counts,
'test_acc': acc,
}
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
val_loss_mean = torch.stack([x['val_loss'] for x in outputs]).mean()
correct_counts = torch.stack([x['val_correct_counts'] for x in outputs]).sum(axis=0)
total_counts = torch.stack([x['val_total_counts'] for x in outputs]).sum(axis=0)
self._accuracy.correct_counts_k = correct_counts
self._accuracy.total_counts_k = total_counts
topk_scores = self._accuracy.compute()
self._accuracy.reset()
tensorboard_log = {'val_loss': val_loss_mean}
for top_k, score in zip(self._accuracy.top_k, topk_scores):
tensorboard_log['val_epoch_top@{}'.format(top_k)] = score
return {'log': tensorboard_log}
def multi_test_epoch_end(self, outputs, dataloader_idx: int = 0):
test_loss_mean = torch.stack([x['test_loss'] for x in outputs]).mean()
correct_counts = torch.stack([x['test_correct_counts'].unsqueeze(0) for x in outputs]).sum(axis=0)
total_counts = torch.stack([x['test_total_counts'].unsqueeze(0) for x in outputs]).sum(axis=0)
self._accuracy.correct_counts_k = correct_counts
self._accuracy.total_counts_k = total_counts
topk_scores = self._accuracy.compute()
self._accuracy.reset()
tensorboard_log = {'test_loss': test_loss_mean}
for top_k, score in zip(self._accuracy.top_k, topk_scores):
tensorboard_log['test_epoch_top@{}'.format(top_k)] = score
return {'log': tensorboard_log}
@typecheck()
def forward(self, input_signal, input_signal_length):
logits = super().forward(input_signal=input_signal, input_signal_length=input_signal_length)
return logits
def change_labels(self, new_labels: List[str]):
"""
Changes labels used by the decoder model. Use this method when fine-tuning on from pre-trained model.
This method changes only decoder and leaves encoder and pre-processing modules unchanged. For example, you would
use it if you want to use pretrained encoder when fine-tuning on a data in another dataset.
If new_labels == self.decoder.vocabulary then nothing will be changed.
Args:
new_labels: list with new labels. Must contain at least 2 elements. Typically, \
this is set of labels for the dataset.
Returns: None
"""
if new_labels is not None and not isinstance(new_labels, ListConfig):
new_labels = ListConfig(new_labels)
if self._cfg.labels == new_labels:
logging.warning(
f"Old labels ({self._cfg.labels}) and new labels ({new_labels}) match. Not changing anything"
)
else:
if new_labels is None or len(new_labels) == 0:
raise ValueError(f'New labels must be non-empty list of labels. But I got: {new_labels}')
# Update config
self._cfg.labels = new_labels
decoder_config = self.decoder.to_config_dict()
new_decoder_config = copy.deepcopy(decoder_config)
self._update_decoder_config(new_labels, new_decoder_config)
del self.decoder
self.decoder = EncDecClassificationModel.from_config_dict(new_decoder_config)
OmegaConf.set_struct(self._cfg.decoder, False)
self._cfg.decoder = new_decoder_config
OmegaConf.set_struct(self._cfg.decoder, True)
if 'train_ds' in self._cfg and self._cfg.train_ds is not None:
self._cfg.train_ds.labels = new_labels
if 'validation_ds' in self._cfg and self._cfg.validation_ds is not None:
self._cfg.validation_ds.labels = new_labels
if 'test_ds' in self._cfg and self._cfg.test_ds is not None:
self._cfg.test_ds.labels = new_labels
logging.info(f"Changed decoder output to {self.decoder.num_classes} labels.")
def _update_decoder_config(self, labels, cfg):
"""
Update the number of classes in the decoder based on labels provided.
Args:
labels: The current labels of the model
cfg: The config of the decoder which will be updated.
"""
OmegaConf.set_struct(cfg, False)
if 'params' in cfg:
cfg.params.num_classes = len(labels)
else:
cfg.num_classes = len(labels)
OmegaConf.set_struct(cfg, True)
class EncDecRegressionModel(_EncDecBaseModel):
"""Encoder decoder class for speech regression models.
Model class creates training, validation methods for setting up data
performing model forward pass.
"""
@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.
"""
result = []
return result
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
if not cfg.get('is_regression_task', False):
raise ValueError(f"EndDecRegressionModel requires the flag is_regression_task to be set as true")
super().__init__(cfg=cfg, trainer=trainer)
def _setup_preprocessor(self):
return EncDecRegressionModel.from_config_dict(self._cfg.preprocessor)
def _setup_encoder(self):
return EncDecRegressionModel.from_config_dict(self._cfg.encoder)
def _setup_decoder(self):
return EncDecRegressionModel.from_config_dict(self._cfg.decoder)
def _setup_loss(self):
return MSELoss()
def _setup_metrics(self):
self._mse = MeanSquaredError()
self._mae = MeanAbsoluteError()
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {"preds": NeuralType(tuple('B'), RegressionValuesType())}
@typecheck()
def forward(self, input_signal, input_signal_length):
logits = super().forward(input_signal=input_signal, input_signal_length=input_signal_length)
return logits.view(-1)
# PTL-specific methods
def training_step(self, batch, batch_idx):
self.training_step_end()
audio_signal, audio_signal_len, targets, targets_len = batch
logits = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss = self.loss(preds=logits, labels=targets)
train_mse = self._mse(preds=logits, target=targets)
train_mae = self._mae(preds=logits, target=targets)
tensorboard_logs = {
'train_loss': loss,
'train_mse': train_mse,
'train_mae': train_mae,
'learning_rate': self._optimizer.param_groups[0]['lr'],
}
self.log_dict(tensorboard_logs)
return {'loss': loss}
def validation_step(self, batch, batch_idx, dataloader_idx: int = 0):
audio_signal, audio_signal_len, targets, targets_len = batch
logits = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss_value = self.loss(preds=logits, labels=targets)
val_mse = self._mse(preds=logits, target=targets)
val_mae = self._mae(preds=logits, target=targets)
return {'val_loss': loss_value, 'val_mse': val_mse, 'val_mae': val_mae}
def test_step(self, batch, batch_idx, dataloader_idx: int = 0):
logs = self.validation_step(batch, batch_idx, dataloader_idx)
return {'test_loss': logs['val_loss'], 'test_mse': logs['test_mse'], 'test_mae': logs['val_mae']}
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
val_loss_mean = torch.stack([x['val_loss'] for x in outputs]).mean()
val_mse = self._mse.compute()
self._mse.reset()
val_mae = self._mae.compute()
self._mae.reset()
tensorboard_logs = {'val_loss': val_loss_mean, 'val_mse': val_mse, 'val_mae': val_mae}
return {'val_loss': val_loss_mean, 'val_mse': val_mse, 'val_mae': val_mae, 'log': tensorboard_logs}
def multi_test_epoch_end(self, outputs, dataloader_idx: int = 0):
test_loss_mean = torch.stack([x['test_loss'] for x in outputs]).mean()
test_mse = self._mse.compute()
self._mse.reset()
test_mae = self._mae.compute()
self._mae.reset()
tensorboard_logs = {'test_loss': test_loss_mean, 'test_mse': test_mse, 'test_mae': test_mae}
return {'test_loss': test_loss_mean, 'test_mse': test_mse, 'test_mae': test_mae, 'log': tensorboard_logs}
@torch.no_grad()
def transcribe(self, paths2audio_files: List[str], batch_size: int = 4) -> List[float]:
"""
Generate class labels for provided audio files. Use this method for debugging and prototyping.
Args:
paths2audio_files: (a list) of paths to audio files. \
Recommended length per file is approximately 1 second.
batch_size: (int) batch size to use during inference. \
Bigger will result in better throughput performance but would use more memory.
Returns:
A list of predictions in the same order as paths2audio_files
"""
predictions = super().transcribe(paths2audio_files, batch_size, logprobs=True)
return [float(pred) for pred in predictions]
def _update_decoder_config(self, labels, cfg):
OmegaConf.set_struct(cfg, False)
if 'params' in cfg:
cfg.params.num_classes = 1
else:
cfg.num_classes = 1
OmegaConf.set_struct(cfg, True)
@@ -0,0 +1,439 @@
# Copyright (c) 2020, 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 json
import os
import pickle as pkl
import shutil
import tarfile
import tempfile
from collections import defaultdict
from typing import List, Optional
import torch
from omegaconf import DictConfig, OmegaConf, open_dict
from omegaconf.listconfig import ListConfig
from pytorch_lightning.utilities import rank_zero_only
from tqdm import tqdm
from nemo.collections.asr.models.classification_models import EncDecClassificationModel
from nemo.collections.asr.models.label_models import ExtractSpeakerEmbeddingsModel
from nemo.collections.asr.parts.mixins.mixins import DiarizationMixin
from nemo.collections.asr.parts.utils.speaker_utils import audio_rttm_map, perform_diarization, write_rttm2manifest
from nemo.collections.asr.parts.utils.vad_utils import (
generate_overlap_vad_seq,
generate_vad_segment_table,
get_vad_stream_status,
prepare_manifest,
)
from nemo.core.classes import Model
from nemo.utils import logging, model_utils
from nemo.utils.exp_manager import NotFoundError
try:
from torch.cuda.amp import autocast
except ImportError:
from contextlib import contextmanager
@contextmanager
def autocast(enabled=None):
yield
__all__ = ['ClusteringDiarizer']
_MODEL_CONFIG_YAML = "model_config.yaml"
_VAD_MODEL = "vad_model.nemo"
_SPEAKER_MODEL = "speaker_model.nemo"
def get_available_model_names(class_name):
available_models = class_name.list_available_models()
return list(map(lambda x: x.pretrained_model_name, available_models))
class ClusteringDiarizer(Model, DiarizationMixin):
def __init__(self, cfg: DictConfig):
cfg = model_utils.convert_model_config_to_dict_config(cfg)
# Convert config to support Hydra 1.0+ instantiation
cfg = model_utils.maybe_update_config_version(cfg)
self._cfg = cfg
self._out_dir = self._cfg.diarizer.out_dir
if not os.path.exists(self._out_dir):
os.mkdir(self._out_dir)
# init vad model
self.has_vad_model = False
self.has_vad_model_to_save = False
self._speaker_manifest_path = self._cfg.diarizer.speaker_embeddings.oracle_vad_manifest
self.AUDIO_RTTM_MAP = None
self.paths2audio_files = self._cfg.diarizer.paths2audio_files
if self._cfg.diarizer.vad.model_path is not None:
self._init_vad_model()
self._vad_dir = os.path.join(self._out_dir, 'vad_outputs')
self._vad_out_file = os.path.join(self._vad_dir, "vad_out.json")
shutil.rmtree(self._vad_dir, ignore_errors=True)
os.makedirs(self._vad_dir)
# init speaker model
self._init_speaker_model()
if self._cfg.diarizer.get('num_speakers', None):
self._num_speakers = self._cfg.diarizer.num_speakers
logging.warning("in next release num_speakers will be changed to oracle_num_speakers")
else:
self._num_speakers = self._cfg.diarizer.oracle_num_speakers
if self._cfg.diarizer.get('max_num_speakers', None):
self.max_num_speakers = self._cfg.diarizer.max_num_speakers
else:
self.max_num_speakers = 8
self._device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
@classmethod
def list_available_models(cls):
pass
def _init_speaker_model(self):
model_path = self._cfg.diarizer.speaker_embeddings.model_path
if model_path is not None and model_path.endswith('.nemo'):
self._speaker_model = ExtractSpeakerEmbeddingsModel.restore_from(model_path)
logging.info("Speaker Model restored locally from {}".format(model_path))
else:
if model_path not in get_available_model_names(ExtractSpeakerEmbeddingsModel):
logging.warning(
"requested {} model name not available in pretrained models, instead".format(model_path)
)
model_path = "speakerdiarization_speakernet"
logging.info("Loading pretrained {} model from NGC".format(model_path))
self._speaker_model = ExtractSpeakerEmbeddingsModel.from_pretrained(model_name=model_path)
self._speaker_dir = os.path.join(self._out_dir, 'speaker_outputs')
def set_vad_model(self, vad_config):
with open_dict(self._cfg):
self._cfg.diarizer.vad = vad_config
self._init_vad_model()
def _init_vad_model(self):
model_path = self._cfg.diarizer.vad.model_path
if model_path.endswith('.nemo'):
self._vad_model = EncDecClassificationModel.restore_from(model_path)
logging.info("VAD model loaded locally from {}".format(model_path))
else:
if model_path not in get_available_model_names(EncDecClassificationModel):
logging.warning(
"requested {} model name not available in pretrained models, instead".format(model_path)
)
model_path = "vad_telephony_marblenet"
logging.info("Loading pretrained {} model from NGC".format(model_path))
self._vad_model = EncDecClassificationModel.from_pretrained(model_name=model_path)
self._vad_window_length_in_sec = self._cfg.diarizer.vad.window_length_in_sec
self._vad_shift_length_in_sec = self._cfg.diarizer.vad.shift_length_in_sec
self.has_vad_model_to_save = True
self.has_vad_model = True
def _setup_vad_test_data(self, manifest_vad_input):
vad_dl_config = {
'manifest_filepath': manifest_vad_input,
'sample_rate': self._cfg.sample_rate,
'vad_stream': True,
'labels': ['infer',],
'time_length': self._vad_window_length_in_sec,
'shift_length': self._vad_shift_length_in_sec,
'trim_silence': False,
'num_workers': self._cfg.num_workers,
}
self._vad_model.setup_test_data(test_data_config=vad_dl_config)
def _setup_spkr_test_data(self, manifest_file):
spk_dl_config = {
'manifest_filepath': manifest_file,
'sample_rate': self._cfg.sample_rate,
'batch_size': 1,
'time_length': self._cfg.diarizer.speaker_embeddings.window_length_in_sec,
'shift_length': self._cfg.diarizer.speaker_embeddings.shift_length_in_sec,
'trim_silence': False,
'embedding_dir': self._speaker_dir,
'labels': None,
'task': "diarization",
'num_workers': self._cfg.num_workers,
}
self._speaker_model.setup_test_data(spk_dl_config)
def _run_vad(self, manifest_file):
self._vad_model = self._vad_model.to(self._device)
self._vad_model.eval()
time_unit = int(self._vad_window_length_in_sec / self._vad_shift_length_in_sec)
trunc = int(time_unit / 2)
trunc_l = time_unit - trunc
all_len = 0
data = []
for line in open(manifest_file, 'r'):
file = os.path.basename(json.loads(line)['audio_filepath'])
data.append(os.path.splitext(file)[0])
status = get_vad_stream_status(data)
for i, test_batch in enumerate(tqdm(self._vad_model.test_dataloader())):
test_batch = [x.to(self._device) for x in test_batch]
with autocast():
log_probs = self._vad_model(input_signal=test_batch[0], input_signal_length=test_batch[1])
probs = torch.softmax(log_probs, dim=-1)
pred = probs[:, 1]
if status[i] == 'start':
to_save = pred[:-trunc]
elif status[i] == 'next':
to_save = pred[trunc:-trunc_l]
elif status[i] == 'end':
to_save = pred[trunc_l:]
else:
to_save = pred
all_len += len(to_save)
outpath = os.path.join(self._vad_dir, data[i] + ".frame")
with open(outpath, "a") as fout:
for f in range(len(to_save)):
fout.write('{0:0.4f}\n'.format(to_save[f]))
del test_batch
if status[i] == 'end' or status[i] == 'single':
all_len = 0
if not self._cfg.diarizer.vad.vad_decision_smoothing:
# Shift the window by 10ms to generate the frame and use the prediction of the window to represent the label for the frame;
self.vad_pred_dir = self._vad_dir
else:
# Generate predictions with overlapping input segments. Then a smoothing filter is applied to decide the label for a frame spanned by multiple segments.
# smoothing_method would be either in majority vote (median) or average (mean)
logging.info("Generating predictions with overlapping input segments")
smoothing_pred_dir = generate_overlap_vad_seq(
frame_pred_dir=self._vad_dir,
smoothing_method=self._cfg.diarizer.vad.smoothing_params.method,
overlap=self._cfg.diarizer.vad.smoothing_params.overlap,
seg_len=self._vad_window_length_in_sec,
shift_len=self._vad_shift_length_in_sec,
num_workers=self._cfg.num_workers,
)
self.vad_pred_dir = smoothing_pred_dir
logging.info("Converting frame level prediction to speech/no-speech segment in start and end times format.")
table_out_dir = generate_vad_segment_table(
vad_pred_dir=self.vad_pred_dir,
threshold=self._cfg.diarizer.vad.threshold,
shift_len=self._vad_shift_length_in_sec,
num_workers=self._cfg.num_workers,
)
vad_table_list = [os.path.join(table_out_dir, key + ".txt") for key in self.AUDIO_RTTM_MAP]
write_rttm2manifest(self._cfg.diarizer.paths2audio_files, vad_table_list, self._vad_out_file)
self._speaker_manifest_path = self._vad_out_file
def _extract_embeddings(self, manifest_file):
logging.info("Extracting embeddings for Diarization")
self._setup_spkr_test_data(manifest_file)
uniq_names = []
out_embeddings = defaultdict(list)
self._speaker_model = self._speaker_model.to(self._device)
self._speaker_model.eval()
with open(manifest_file, 'r') as manifest:
for line in manifest.readlines():
line = line.strip()
dic = json.loads(line)
uniq_names.append(dic['audio_filepath'].split('/')[-1].rsplit('.', 1)[0])
for i, test_batch in enumerate(tqdm(self._speaker_model.test_dataloader())):
test_batch = [x.to(self._device) for x in test_batch]
audio_signal, audio_signal_len, labels, slices = test_batch
with autocast():
_, embs = self._speaker_model.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
emb_shape = embs.shape[-1]
embs = embs.view(-1, emb_shape).type(torch.float32)
embs = embs.cpu().detach().numpy()
out_embeddings[uniq_names[i]].extend(embs)
del test_batch
embedding_dir = os.path.join(self._speaker_dir, 'embeddings')
if not os.path.exists(embedding_dir):
os.makedirs(embedding_dir, exist_ok=True)
prefix = manifest_file.split('/')[-1].rsplit('.', 1)[-2]
name = os.path.join(embedding_dir, prefix)
self._embeddings_file = name + '_embeddings.pkl'
pkl.dump(out_embeddings, open(self._embeddings_file, 'wb'))
logging.info("Saved embedding files to {}".format(embedding_dir))
def path2audio_files_to_manifest(self, paths2audio_files):
mfst_file = os.path.join(self._out_dir, 'manifest.json')
with open(mfst_file, 'w') as fp:
for audio_file in paths2audio_files:
audio_file = audio_file.strip()
entry = {'audio_filepath': audio_file, 'offset': 0.0, 'duration': None, 'text': '-', 'label': 'infer'}
fp.write(json.dumps(entry) + '\n')
return mfst_file
def diarize(self, paths2audio_files: List[str] = None, batch_size: int = 1):
"""
"""
if paths2audio_files:
self.paths2audio_files = paths2audio_files
else:
if self._cfg.diarizer.paths2audio_files is None:
raise ValueError("Pass path2audio files either through config or to diarize method")
else:
self.paths2audio_files = self._cfg.diarizer.paths2audio_files
if type(self.paths2audio_files) is str and os.path.isfile(self.paths2audio_files):
paths2audio_files = []
with open(self.paths2audio_files, 'r') as path2file:
for audiofile in path2file.readlines():
audiofile = audiofile.strip()
paths2audio_files.append(audiofile)
elif type(self.paths2audio_files) in [list, ListConfig]:
paths2audio_files = list(self.paths2audio_files)
else:
raise ValueError("paths2audio_files must be of type list or path to file containing audio files")
self.AUDIO_RTTM_MAP = audio_rttm_map(paths2audio_files, self._cfg.diarizer.path2groundtruth_rttm_files)
if self.has_vad_model:
logging.info("Performing VAD")
mfst_file = self.path2audio_files_to_manifest(paths2audio_files)
self._dont_auto_split = False
self._split_duration = 50
manifest_vad_input = mfst_file
if not self._dont_auto_split:
logging.info("Split long audio file to avoid CUDA memory issue")
logging.debug("Try smaller split_duration if you still have CUDA memory issue")
config = {
'manifest_filepath': mfst_file,
'time_length': self._vad_window_length_in_sec,
'split_duration': self._split_duration,
'num_workers': self._cfg.num_workers,
}
manifest_vad_input = prepare_manifest(config)
else:
logging.warning(
"If you encounter CUDA memory issue, try splitting manifest entry by split_duration to avoid it."
)
self._setup_vad_test_data(manifest_vad_input)
self._run_vad(manifest_vad_input)
else:
if not os.path.exists(self._speaker_manifest_path):
raise NotFoundError("Oracle VAD based manifest file not found")
self._extract_embeddings(self._speaker_manifest_path)
out_rttm_dir = os.path.join(self._out_dir, 'pred_rttms')
os.makedirs(out_rttm_dir, exist_ok=True)
perform_diarization(
embeddings_file=self._embeddings_file,
reco2num=self._num_speakers,
manifest_path=self._speaker_manifest_path,
sample_rate=self._cfg.sample_rate,
window=self._cfg.diarizer.speaker_embeddings.window_length_in_sec,
shift=self._cfg.diarizer.speaker_embeddings.shift_length_in_sec,
audio_rttm_map=self.AUDIO_RTTM_MAP,
out_rttm_dir=out_rttm_dir,
max_num_speakers=self.max_num_speakers,
)
@staticmethod
def __make_nemo_file_from_folder(filename, source_dir):
with tarfile.open(filename, "w:gz") as tar:
tar.add(source_dir, arcname="./")
@rank_zero_only
def save_to(self, save_path: str):
"""
Saves model instance (weights and configuration) into EFF archive or .
You can use "restore_from" method to fully restore instance from .nemo file.
.nemo file is an archive (tar.gz) with the following:
model_config.yaml - model configuration in .yaml format. You can deserialize this into cfg argument for model's constructor
model_wights.chpt - model checkpoint
Args:
save_path: Path to .nemo file where model instance should be saved
"""
with tempfile.TemporaryDirectory() as tmpdir:
config_yaml = os.path.join(tmpdir, _MODEL_CONFIG_YAML)
spkr_model = os.path.join(tmpdir, _SPEAKER_MODEL)
self.to_config_file(path2yaml_file=config_yaml)
if self.has_vad_model:
vad_model = os.path.join(tmpdir, _VAD_MODEL)
self._vad_model.save_to(vad_model)
self._speaker_model.save_to(spkr_model)
self.__make_nemo_file_from_folder(filename=save_path, source_dir=tmpdir)
@staticmethod
def __unpack_nemo_file(path2file: str, out_folder: str) -> str:
if not os.path.exists(path2file):
raise FileNotFoundError(f"{path2file} does not exist")
tar = tarfile.open(path2file, "r:gz")
tar.extractall(path=out_folder)
tar.close()
return out_folder
@classmethod
def restore_from(
cls,
restore_path: str,
override_config_path: Optional[str] = None,
map_location: Optional[torch.device] = None,
strict: bool = False,
):
# Get path where the command is executed - the artifacts will be "retrieved" there
# (original .nemo behavior)
cwd = os.getcwd()
with tempfile.TemporaryDirectory() as tmpdir:
try:
cls.__unpack_nemo_file(path2file=restore_path, out_folder=tmpdir)
os.chdir(tmpdir)
if override_config_path is None:
config_yaml = os.path.join(tmpdir, _MODEL_CONFIG_YAML)
else:
config_yaml = override_config_path
conf = OmegaConf.load(config_yaml)
if os.path.exists(os.path.join(tmpdir, _VAD_MODEL)):
conf.diarizer.vad.model_path = os.path.join(tmpdir, _VAD_MODEL)
else:
logging.info(
f'Model {cls.__name__} does not contain a VAD model. A VAD model or manifest file with'
f'speech segments need for diarization with this model'
)
conf.diarizer.speaker_embeddings.model_path = os.path.join(tmpdir, _SPEAKER_MODEL)
conf.restore_map_location = map_location
OmegaConf.set_struct(conf, True)
instance = cls(cfg=conf)
logging.info(f'Model {cls.__name__} was successfully restored from {restore_path}.')
finally:
os.chdir(cwd)
return instance
@@ -0,0 +1,46 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.models.configs.classification_models_config import (
EncDecClassificationConfig,
EncDecClassificationDatasetConfig,
EncDecClassificationModelConfig,
)
from nemo.collections.asr.models.configs.ctc_models_config import (
EncDecCTCConfig,
EncDecCTCDatasetConfig,
EncDecCTCModelConfig,
)
from nemo.collections.asr.models.configs.matchboxnet_config import (
EncDecClassificationModelConfigBuilder,
MatchboxNetModelConfig,
MatchboxNetVADModelConfig,
)
from nemo.collections.asr.models.configs.quartznet_config import (
EncDecCTCModelConfigBuilder,
JasperModelConfig,
QuartzNetModelConfig,
)
from nemo.collections.asr.modules.audio_preprocessing import (
AudioToMelSpectrogramPreprocessorConfig,
AudioToMFCCPreprocessorConfig,
CropOrPadSpectrogramAugmentationConfig,
SpectrogramAugmentationConfig,
)
from nemo.collections.asr.modules.conv_asr import (
ConvASRDecoderClassificationConfig,
ConvASRDecoderConfig,
ConvASREncoderConfig,
JasperEncoderConfig,
)
@@ -0,0 +1,95 @@
# Copyright (c) 2020, 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.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from omegaconf import MISSING
import nemo.core.classes.dataset
from nemo.collections.asr.modules.audio_preprocessing import (
AudioToMFCCPreprocessorConfig,
CropOrPadSpectrogramAugmentationConfig,
SpectrogramAugmentationConfig,
)
from nemo.collections.asr.modules.conv_asr import ConvASRDecoderClassificationConfig, ConvASREncoderConfig
from nemo.core.config import modelPT as model_cfg
@dataclass
class EncDecClassificationDatasetConfig(nemo.core.classes.dataset.DatasetConfig):
manifest_filepath: Optional[str] = None
sample_rate: int = MISSING
labels: List[str] = MISSING
trim_silence: bool = False
# Tarred dataset support
is_tarred: bool = False
tarred_audio_filepaths: Optional[str] = None
tarred_shard_strategy: str = "scatter"
shuffle_n: int = 0
# Optional
int_values: Optional[int] = None
augmentor: Optional[Dict[str, Any]] = None
max_duration: Optional[float] = None
min_duration: Optional[float] = None
# VAD Optional
vad_stream: Optional[bool] = None
time_length: float = 0.31
shift_length: float = 0.01
normalize_audio: bool = False
is_regression_task: bool = False
@dataclass
class EncDecClassificationConfig(model_cfg.ModelConfig):
# Model global arguments
sample_rate: int = 16000
repeat: int = 1
dropout: float = 0.0
separable: bool = True
kernel_size_factor: float = 1.0
labels: List[str] = MISSING
timesteps: int = MISSING
# Dataset configs
train_ds: EncDecClassificationDatasetConfig = EncDecClassificationDatasetConfig(
manifest_filepath=None, shuffle=True, trim_silence=False
)
validation_ds: EncDecClassificationDatasetConfig = EncDecClassificationDatasetConfig(
manifest_filepath=None, shuffle=False
)
test_ds: EncDecClassificationDatasetConfig = EncDecClassificationDatasetConfig(
manifest_filepath=None, shuffle=False
)
# Optimizer / Scheduler config
optim: Optional[model_cfg.OptimConfig] = model_cfg.OptimConfig(sched=model_cfg.SchedConfig())
# Model component configs
preprocessor: AudioToMFCCPreprocessorConfig = AudioToMFCCPreprocessorConfig()
spec_augment: Optional[SpectrogramAugmentationConfig] = SpectrogramAugmentationConfig()
crop_or_pad_augment: Optional[CropOrPadSpectrogramAugmentationConfig] = CropOrPadSpectrogramAugmentationConfig(
audio_length=timesteps
)
encoder: ConvASREncoderConfig = ConvASREncoderConfig()
decoder: ConvASRDecoderClassificationConfig = ConvASRDecoderClassificationConfig()
@dataclass
class EncDecClassificationModelConfig(model_cfg.NemoConfig):
model: EncDecClassificationConfig = EncDecClassificationConfig()
@@ -0,0 +1,85 @@
# Copyright (c) 2020, 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.
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from omegaconf import MISSING
import nemo.core.classes.dataset
from nemo.collections.asr.modules.audio_preprocessing import (
AudioToMelSpectrogramPreprocessorConfig,
SpectrogramAugmentationConfig,
)
from nemo.collections.asr.modules.conv_asr import ConvASRDecoderConfig, ConvASREncoderConfig
from nemo.core.config import modelPT as model_cfg
@dataclass
class EncDecCTCDatasetConfig(nemo.core.classes.dataset.DatasetConfig):
manifest_filepath: Optional[str] = None
sample_rate: int = MISSING
labels: List[str] = MISSING
trim_silence: bool = False
# Tarred dataset support
is_tarred: bool = False
tarred_audio_filepaths: Optional[str] = None
tarred_shard_strategy: str = "scatter"
shuffle_n: int = 0
# Optional
int_values: Optional[int] = None
augmentor: Optional[Dict[str, Any]] = None
max_duration: Optional[float] = None
min_duration: Optional[float] = None
max_utts: int = 0
blank_index: int = -1
unk_index: int = -1
normalize: bool = False
trim: bool = True
parser: Optional[str] = 'en'
eos_id: Optional[int] = None
bos_id: Optional[int] = None
pad_id: int = 0
use_start_end_token: bool = False
@dataclass
class EncDecCTCConfig(model_cfg.ModelConfig):
# Model global arguments
sample_rate: int = 16000
repeat: int = 1
dropout: float = 0.0
separable: bool = False
labels: List[str] = MISSING
# Dataset configs
train_ds: EncDecCTCDatasetConfig = EncDecCTCDatasetConfig(manifest_filepath=None, shuffle=True)
validation_ds: EncDecCTCDatasetConfig = EncDecCTCDatasetConfig(manifest_filepath=None, shuffle=False)
test_ds: EncDecCTCDatasetConfig = EncDecCTCDatasetConfig(manifest_filepath=None, shuffle=False)
# Optimizer / Scheduler config
optim: Optional[model_cfg.OptimConfig] = model_cfg.OptimConfig(sched=model_cfg.SchedConfig())
# Model component configs
preprocessor: AudioToMelSpectrogramPreprocessorConfig = AudioToMelSpectrogramPreprocessorConfig()
spec_augment: Optional[SpectrogramAugmentationConfig] = SpectrogramAugmentationConfig()
encoder: ConvASREncoderConfig = ConvASREncoderConfig()
decoder: ConvASRDecoderConfig = ConvASRDecoderConfig()
@dataclass
class EncDecCTCModelConfig(model_cfg.NemoConfig):
model: EncDecCTCConfig = EncDecCTCConfig()
@@ -0,0 +1,253 @@
# Copyright (c) 2020, 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.
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional
from omegaconf import MISSING
from nemo.collections.asr.models.configs import classification_models_config as clf_cfg
from nemo.collections.asr.modules.audio_preprocessing import (
AudioToMFCCPreprocessorConfig,
CropOrPadSpectrogramAugmentationConfig,
SpectrogramAugmentationConfig,
)
from nemo.collections.asr.modules.conv_asr import (
ConvASRDecoderClassificationConfig,
ConvASREncoderConfig,
JasperEncoderConfig,
)
from nemo.core.config import modelPT as model_cfg
# fmt: off
def matchboxnet_3x1x64():
config = [
JasperEncoderConfig(filters=128, repeat=1, kernel=[11], stride=[1], dilation=[1], dropout=0.0,
residual=False, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=64, repeat=1, kernel=[13], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=64, repeat=1, kernel=[15], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=64, repeat=1, kernel=[17], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=128, repeat=1, kernel=[29], stride=[1], dilation=[2], dropout=0.0,
residual=False, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=128, repeat=1, kernel=[1], stride=[1], dilation=[1], dropout=0.0,
residual=False, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False)
]
return config
def matchboxnet_3x1x64_vad():
config = [
JasperEncoderConfig(filters=128, repeat=1, kernel=[11], stride=[1], dilation=[1], dropout=0.0,
residual=False, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=64, repeat=1, kernel=[13], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=64, repeat=1, kernel=[15], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=64, repeat=1, kernel=[17], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=128, repeat=1, kernel=[29], stride=[1], dilation=[2], dropout=0.0,
residual=False, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=128, repeat=1, kernel=[1], stride=[1], dilation=[1], dropout=0.0,
residual=False, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False)
]
return config
# fmt: on
@dataclass
class MatchboxNetModelConfig(clf_cfg.EncDecClassificationConfig):
# Model global arguments
sample_rate: int = 16000
repeat: int = 1
dropout: float = 0.0
separable: bool = True
kernel_size_factor: float = 1.0
timesteps: int = 128
labels: List[str] = MISSING
# Dataset configs
train_ds: clf_cfg.EncDecClassificationDatasetConfig = clf_cfg.EncDecClassificationDatasetConfig(
manifest_filepath=None, shuffle=True, trim_silence=False
)
validation_ds: clf_cfg.EncDecClassificationDatasetConfig = clf_cfg.EncDecClassificationDatasetConfig(
manifest_filepath=None, shuffle=False
)
test_ds: clf_cfg.EncDecClassificationDatasetConfig = clf_cfg.EncDecClassificationDatasetConfig(
manifest_filepath=None, shuffle=False
)
# Optimizer / Scheduler config
optim: Optional[model_cfg.OptimConfig] = model_cfg.OptimConfig(sched=model_cfg.SchedConfig())
# Model general component configs
preprocessor: AudioToMFCCPreprocessorConfig = AudioToMFCCPreprocessorConfig(window_size=0.025)
spec_augment: Optional[SpectrogramAugmentationConfig] = SpectrogramAugmentationConfig(
freq_masks=2, time_masks=2, freq_width=15, time_width=25, rect_masks=5, rect_time=25, rect_freq=15
)
crop_or_pad_augment: Optional[CropOrPadSpectrogramAugmentationConfig] = CropOrPadSpectrogramAugmentationConfig(
audio_length=128
)
encoder: ConvASREncoderConfig = ConvASREncoderConfig(activation="relu")
decoder: ConvASRDecoderClassificationConfig = ConvASRDecoderClassificationConfig()
@dataclass
class MatchboxNetVADModelConfig(MatchboxNetModelConfig):
timesteps: int = 64
labels: List[str] = field(default_factory=lambda: ['background', 'speech'])
crop_or_pad_augment: Optional[CropOrPadSpectrogramAugmentationConfig] = None
class EncDecClassificationModelConfigBuilder(model_cfg.ModelConfigBuilder):
VALID_CONFIGS = ['matchboxnet_3x1x64', 'matchboxnet_3x1x64_vad']
def __init__(self, name: str = 'matchboxnet_3x1x64', encoder_cfg_func: Optional[Callable[[], List[Any]]] = None):
if name not in EncDecClassificationModelConfigBuilder.VALID_CONFIGS:
raise ValueError("`name` must be one of : \n" f"{EncDecClassificationModelConfigBuilder.VALID_CONFIGS}")
self.name = name
if 'matchboxnet_3x1x64_vad' in name:
if encoder_cfg_func is None:
encoder_cfg_func = matchboxnet_3x1x64_vad
model_cfg = MatchboxNetVADModelConfig(
repeat=1,
separable=True,
encoder=ConvASREncoderConfig(jasper=encoder_cfg_func(), activation="relu"),
decoder=ConvASRDecoderClassificationConfig(),
)
elif 'matchboxnet_3x1x64' in name:
if encoder_cfg_func is None:
encoder_cfg_func = matchboxnet_3x1x64
model_cfg = MatchboxNetModelConfig(
repeat=1,
separable=False,
spec_augment=SpectrogramAugmentationConfig(rect_masks=5, rect_freq=50, rect_time=120),
encoder=ConvASREncoderConfig(jasper=encoder_cfg_func(), activation="relu"),
decoder=ConvASRDecoderClassificationConfig(),
)
else:
raise ValueError(f"Invalid config name submitted to {self.__class__.__name__}")
super(EncDecClassificationModelConfigBuilder, self).__init__(model_cfg)
self.model_cfg: clf_cfg.EncDecClassificationConfig = model_cfg # enable type hinting
def set_labels(self, labels: List[str]):
self.model_cfg.labels = labels
def set_separable(self, separable: bool):
self.model_cfg.separable = separable
def set_repeat(self, repeat: int):
self.model_cfg.repeat = repeat
def set_sample_rate(self, sample_rate: int):
self.model_cfg.sample_rate = sample_rate
def set_dropout(self, dropout: float = 0.0):
self.model_cfg.dropout = dropout
def set_timesteps(self, timesteps: int):
self.model_cfg.timesteps = timesteps
def set_is_regression_task(self, is_regression_task: bool):
self.model_cfg.is_regression_task = is_regression_task
# Note: Autocomplete for users wont work without these overrides
# But practically it is not needed since python will infer at runtime
# def set_train_ds(self, cfg: Optional[clf_cfg.EncDecClassificationDatasetConfig] = None):
# super().set_train_ds(cfg)
#
# def set_validation_ds(self, cfg: Optional[clf_cfg.EncDecClassificationDatasetConfig] = None):
# super().set_validation_ds(cfg)
#
# def set_test_ds(self, cfg: Optional[clf_cfg.EncDecClassificationDatasetConfig] = None):
# super().set_test_ds(cfg)
def _finalize_cfg(self):
# propagate labels
self.model_cfg.train_ds.labels = self.model_cfg.labels
self.model_cfg.validation_ds.labels = self.model_cfg.labels
self.model_cfg.test_ds.labels = self.model_cfg.labels
self.model_cfg.decoder.vocabulary = self.model_cfg.labels
# propagate num classes
self.model_cfg.decoder.num_classes = len(self.model_cfg.labels)
# propagate sample rate
self.model_cfg.sample_rate = self.model_cfg.sample_rate
self.model_cfg.preprocessor.sample_rate = self.model_cfg.sample_rate
self.model_cfg.train_ds.sample_rate = self.model_cfg.sample_rate
self.model_cfg.validation_ds.sample_rate = self.model_cfg.sample_rate
self.model_cfg.test_ds.sample_rate = self.model_cfg.sample_rate
# propagate filters
self.model_cfg.encoder.feat_in = self.model_cfg.preprocessor.features
self.model_cfg.decoder.feat_in = self.model_cfg.encoder.jasper[-1].filters
# propagate timeteps
if self.model_cfg.crop_or_pad_augment is not None:
self.model_cfg.crop_or_pad_augment.audio_length = self.model_cfg.timesteps
# propagate separable
for layer in self.model_cfg.encoder.jasper[:-1]: # type: JasperEncoderConfig
layer.separable = self.model_cfg.separable
# propagate repeat
for layer in self.model_cfg.encoder.jasper[1:-2]: # type: JasperEncoderConfig
layer.repeat = self.model_cfg.repeat
# propagate dropout
for layer in self.model_cfg.encoder.jasper: # type: JasperEncoderConfig
layer.dropout = self.model_cfg.dropout
def build(self) -> clf_cfg.EncDecClassificationConfig:
return super().build()
@@ -0,0 +1,308 @@
# Copyright (c) 2020, 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.
from dataclasses import dataclass
from typing import Any, Callable, List, Optional
from omegaconf import MISSING
from nemo.collections.asr.models.configs import ctc_models_config as ctc_cfg
from nemo.collections.asr.modules.audio_preprocessing import (
AudioToMelSpectrogramPreprocessorConfig,
SpectrogramAugmentationConfig,
)
from nemo.collections.asr.modules.conv_asr import ConvASRDecoderConfig, ConvASREncoderConfig, JasperEncoderConfig
from nemo.core.config import modelPT as model_cfg
# fmt: off
def qn_15x5():
config = [
JasperEncoderConfig(filters=256, repeat=1, kernel=[33], stride=[2], dilation=[1], dropout=0.0,
residual=False, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[33], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[33], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[33], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[39], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[39], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[39], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[51], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[51], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[51], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[63], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[63], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[63], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[75], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[75], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[75], stride=[1], dilation=[1], dropout=0.0,
residual=True, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=1, kernel=[87], stride=[1], dilation=[2], dropout=0.0,
residual=False, groups=1, separable=True, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=1024, repeat=1, kernel=[1], stride=[1], dilation=[1], dropout=0.0,
residual=False, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False)
]
return config
def jasper_10x5_dr():
config = [
JasperEncoderConfig(filters=256, repeat=1, kernel=[11], stride=[2], dilation=[1], dropout=0.2,
residual=False, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[11], stride=[1], dilation=[1], dropout=0.2,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=256, repeat=5, kernel=[11], stride=[1], dilation=[1], dropout=0.2,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=384, repeat=5, kernel=[13], stride=[1], dilation=[1], dropout=0.2,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=384, repeat=5, kernel=[13], stride=[1], dilation=[1], dropout=0.2,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[17], stride=[1], dilation=[1], dropout=0.2,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=512, repeat=5, kernel=[17], stride=[1], dilation=[1], dropout=0.2,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=640, repeat=5, kernel=[21], stride=[1], dilation=[1], dropout=0.3,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=640, repeat=5, kernel=[21], stride=[1], dilation=[1], dropout=0.3,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=768, repeat=5, kernel=[25], stride=[1], dilation=[1], dropout=0.3,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=768, repeat=5, kernel=[25], stride=[1], dilation=[1], dropout=0.3,
residual=True, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=True, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=896, repeat=1, kernel=[29], stride=[1], dilation=[2], dropout=0.4,
residual=False, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False),
JasperEncoderConfig(filters=1024, repeat=1, kernel=[1], stride=[1], dilation=[1], dropout=0.4,
residual=False, groups=1, separable=False, heads=-1, residual_mode='add',
residual_dense=False, se=False, se_reduction_ratio=8, se_context_size=-1,
se_interpolation_mode='nearest', kernel_size_factor=1.0, stride_last=False)
]
return config
# fmt: on
@dataclass
class JasperModelConfig(ctc_cfg.EncDecCTCConfig):
# Model global arguments
sample_rate: int = 16000
repeat: int = 1
dropout: float = 0.0
separable: bool = False
labels: List[str] = MISSING
# Dataset configs
train_ds: ctc_cfg.EncDecCTCDatasetConfig = ctc_cfg.EncDecCTCDatasetConfig(
manifest_filepath=None, shuffle=True, trim_silence=True
)
validation_ds: ctc_cfg.EncDecCTCDatasetConfig = ctc_cfg.EncDecCTCDatasetConfig(
manifest_filepath=None, shuffle=False
)
test_ds: ctc_cfg.EncDecCTCDatasetConfig = ctc_cfg.EncDecCTCDatasetConfig(manifest_filepath=None, shuffle=False)
# Optimizer / Scheduler config
optim: Optional[model_cfg.OptimConfig] = model_cfg.OptimConfig(sched=model_cfg.SchedConfig())
# Model general component configs
preprocessor: AudioToMelSpectrogramPreprocessorConfig = AudioToMelSpectrogramPreprocessorConfig()
spec_augment: Optional[SpectrogramAugmentationConfig] = SpectrogramAugmentationConfig()
encoder: ConvASREncoderConfig = ConvASREncoderConfig(activation="relu")
decoder: ConvASRDecoderConfig = ConvASRDecoderConfig()
@dataclass
class QuartzNetModelConfig(JasperModelConfig):
separable: bool = True
class EncDecCTCModelConfigBuilder(model_cfg.ModelConfigBuilder):
VALID_CONFIGS = ['quartznet_15x5', 'quartznet_15x5_zh', 'jasper_10x5dr']
def __init__(self, name: str = 'quartznet_15x5', encoder_cfg_func: Optional[Callable[[], List[Any]]] = None):
if name not in EncDecCTCModelConfigBuilder.VALID_CONFIGS:
raise ValueError("`name` must be one of : \n" f"{EncDecCTCModelConfigBuilder.VALID_CONFIGS}")
self.name = name
if 'quartznet_15x5' in name:
if encoder_cfg_func is None:
encoder_cfg_func = qn_15x5
model_cfg = QuartzNetModelConfig(
repeat=5,
separable=True,
spec_augment=SpectrogramAugmentationConfig(rect_masks=5, rect_freq=50, rect_time=120),
encoder=ConvASREncoderConfig(jasper=encoder_cfg_func(), activation="relu"),
decoder=ConvASRDecoderConfig(),
)
elif 'jasper_10x5' in name:
if encoder_cfg_func is None:
encoder_cfg_func = jasper_10x5_dr
model_cfg = JasperModelConfig(
repeat=5,
separable=False,
spec_augment=SpectrogramAugmentationConfig(rect_masks=5, rect_freq=50, rect_time=120),
encoder=ConvASREncoderConfig(jasper=encoder_cfg_func(), activation="relu"),
decoder=ConvASRDecoderConfig(),
)
else:
raise ValueError(f"Invalid config name submitted to {self.__class__.__name__}")
super(EncDecCTCModelConfigBuilder, self).__init__(model_cfg)
self.model_cfg: ctc_cfg.EncDecCTCConfig = model_cfg # enable type hinting
if 'zh' in name:
self.set_dataset_normalize(normalize=False)
def set_labels(self, labels: List[str]):
self.model_cfg.labels = labels
def set_separable(self, separable: bool):
self.model_cfg.separable = separable
def set_repeat(self, repeat: int):
self.model_cfg.repeat = repeat
def set_sample_rate(self, sample_rate: int):
self.model_cfg.sample_rate = sample_rate
def set_dropout(self, dropout: float = 0.0):
self.model_cfg.dropout = dropout
def set_dataset_normalize(self, normalize: bool):
self.model_cfg.train_ds.normalize = normalize
self.model_cfg.validation_ds.normalize = normalize
self.model_cfg.test_ds.normalize = normalize
# Note: Autocomplete for users wont work without these overrides
# But practically it is not needed since python will infer at runtime
# def set_train_ds(self, cfg: Optional[ctc_cfg.EncDecCTCDatasetConfig] = None):
# super().set_train_ds(cfg)
#
# def set_validation_ds(self, cfg: Optional[ctc_cfg.EncDecCTCDatasetConfig] = None):
# super().set_validation_ds(cfg)
#
# def set_test_ds(self, cfg: Optional[ctc_cfg.EncDecCTCDatasetConfig] = None):
# super().set_test_ds(cfg)
def _finalize_cfg(self):
# propagate labels
self.model_cfg.train_ds.labels = self.model_cfg.labels
self.model_cfg.validation_ds.labels = self.model_cfg.labels
self.model_cfg.test_ds.labels = self.model_cfg.labels
self.model_cfg.decoder.vocabulary = self.model_cfg.labels
# propagate num classes
self.model_cfg.decoder.num_classes = len(self.model_cfg.labels)
# propagate sample rate
self.model_cfg.sample_rate = self.model_cfg.sample_rate
self.model_cfg.preprocessor.sample_rate = self.model_cfg.sample_rate
self.model_cfg.train_ds.sample_rate = self.model_cfg.sample_rate
self.model_cfg.validation_ds.sample_rate = self.model_cfg.sample_rate
self.model_cfg.test_ds.sample_rate = self.model_cfg.sample_rate
# propagate filters
self.model_cfg.encoder.feat_in = self.model_cfg.preprocessor.features
self.model_cfg.decoder.feat_in = self.model_cfg.encoder.jasper[-1].filters
# propagate separable
for layer in self.model_cfg.encoder.jasper[:-1]: # type: JasperEncoderConfig
layer.separable = self.model_cfg.separable
# propagate repeat
for layer in self.model_cfg.encoder.jasper[1:-2]: # type: JasperEncoderConfig
layer.repeat = self.model_cfg.repeat
# propagate dropout
for layer in self.model_cfg.encoder.jasper: # type: JasperEncoderConfig
layer.dropout = self.model_cfg.dropout
def build(self) -> ctc_cfg.EncDecCTCConfig:
return super().build()
@@ -0,0 +1,304 @@
# Copyright (c) 2020, 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 copy
import os
from typing import Dict, Optional
import torch
from omegaconf import DictConfig, ListConfig, OmegaConf, open_dict
from nemo.collections.asr.data import audio_to_text_dataset
from nemo.collections.asr.losses.ctc import CTCLoss
from nemo.collections.asr.metrics.wer_bpe import WERBPE
from nemo.collections.asr.models.ctc_models import EncDecCTCModel
from nemo.collections.asr.parts.mixins import ASRBPEMixin
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.core.classes.common import PretrainedModelInfo
from nemo.utils import logging, model_utils
__all__ = ['EncDecCTCModelBPE']
class EncDecCTCModelBPE(EncDecCTCModel, ASRBPEMixin):
"""Encoder decoder CTC-based models with Byte Pair Encoding."""
@classmethod
def list_available_models(cls) -> Optional[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.
"""
results = []
model = PretrainedModelInfo(
pretrained_model_name="stt_en_citrinet_256",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_citrinet_256",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_citrinet_256/versions/1.0.0rc1/files/stt_en_citrinet_256.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_citrinet_512",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_citrinet_512",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_citrinet_512/versions/1.0.0rc1/files/stt_en_citrinet_512.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_citrinet_1024",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_citrinet_1024",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_citrinet_1024/versions/1.0.0rc1/files/stt_en_citrinet_1024.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_es_citrinet_512",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_es_citrinet_512",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_es_citrinet_512/versions/1.0.0/files/stt_es_citrinet_512.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_conformer_ctc_small",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_conformer_ctc_small",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_conformer_ctc_small/versions/1.0.0/files/stt_en_conformer_ctc_small.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_conformer_ctc_medium",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_conformer_ctc_medium",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_conformer_ctc_medium/versions/1.0.0/files/stt_en_conformer_ctc_medium.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_conformer_ctc_large",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_conformer_ctc_large",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_conformer_ctc_large/versions/1.0.0/files/stt_en_conformer_ctc_large.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_conformer_ctc_small_ls",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_conformer_ctc_small_ls",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_conformer_ctc_small_ls/versions/1.0.0/files/stt_en_conformer_ctc_small_ls.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_conformer_ctc_medium_ls",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_conformer_ctc_medium_ls",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_conformer_ctc_medium_ls/versions/1.0.0/files/stt_en_conformer_ctc_medium_ls.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_conformer_ctc_large_ls",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_conformer_ctc_large_ls",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_conformer_ctc_large_ls/versions/1.0.0/files/stt_en_conformer_ctc_large_ls.nemo",
)
results.append(model)
return results
def __init__(self, cfg: DictConfig, 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)
if 'tokenizer' not in cfg:
raise ValueError("`cfg` must have `tokenizer` config to create a tokenizer !")
# 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.decoder.vocabulary = ListConfig(list(vocabulary.keys()))
# Override number of classes if placeholder provided
num_classes = cfg.decoder["num_classes"]
if num_classes < 1:
logging.info(
"\nReplacing placeholder number of classes ({}) with actual number of classes - {}".format(
num_classes, len(vocabulary)
)
)
cfg.decoder["num_classes"] = len(vocabulary)
super().__init__(cfg=cfg, trainer=trainer)
# Setup metric objects
self._wer = WERBPE(
tokenizer=self.tokenizer,
batch_dim_index=0,
use_cer=self._cfg.get('use_cer', False),
ctc_decode=True,
dist_sync_on_step=True,
log_prediction=self._cfg.get("log_prediction", False),
)
def _setup_dataloader_from_config(self, config: Optional[Dict]):
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
shuffle = config['shuffle']
# Instantiate tarred dataset loader or normal dataset loader
if config.get('is_tarred', False):
if ('tarred_audio_filepaths' in config and config['tarred_audio_filepaths'] is None) or (
'manifest_filepath' in config and config['manifest_filepath'] is None
):
logging.warning(
"Could not load dataset as `manifest_filepath` was None or "
f"`tarred_audio_filepaths` is None. Provided config : {config}"
)
return None
shuffle_n = config.get('shuffle_n', 4 * config['batch_size']) if shuffle else 0
dataset = audio_to_text_dataset.get_tarred_bpe_dataset(
config=config,
tokenizer=self.tokenizer,
shuffle_n=shuffle_n,
global_rank=self.global_rank,
world_size=self.world_size,
augmentor=augmentor,
)
shuffle = False
else:
if 'manifest_filepath' in config and config['manifest_filepath'] is None:
logging.warning(f"Could not load dataset as `manifest_filepath` was None. Provided config : {config}")
return None
dataset = audio_to_text_dataset.get_bpe_dataset(
config=config, tokenizer=self.tokenizer, augmentor=augmentor
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=config['batch_size'],
collate_fn=dataset.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.
Returns:
A pytorch DataLoader for the given audio file(s).
"""
dl_config = {
'manifest_filepath': os.path.join(config['temp_dir'], 'manifest.json'),
'sample_rate': self.preprocessor._sample_rate,
'batch_size': min(config['batch_size'], len(config['paths2audio_files'])),
'shuffle': False,
}
temporary_datalayer = self._setup_dataloader_from_config(config=DictConfig(dl_config))
return temporary_datalayer
def change_vocabulary(self, new_tokenizer_dir: str, new_tokenizer_type: str):
"""
Changes vocabulary of the tokenizer used during CTC decoding process.
Use this method when fine-tuning on from pre-trained model.
This method changes only decoder and leaves encoder and pre-processing modules unchanged. For example, you would
use it if you want to use pretrained encoder when fine-tuning on a data in another language, or when you'd need
model to learn capitalization, punctuation and/or special characters.
Args:
new_tokenizer_dir: Path to the new tokenizer directory.
new_tokenizer_type: Either `bpe` or `wpe`. `bpe` is used for SentencePiece tokenizers,
whereas `wpe` is used for `BertTokenizer`.
Returns: None
"""
if not os.path.isdir(new_tokenizer_dir):
raise NotADirectoryError(
f'New tokenizer dir must be non-empty path to a directory. But I got: {new_tokenizer_dir}'
)
if new_tokenizer_type.lower() not in ('bpe', 'wpe'):
raise ValueError(f'New tokenizer type must be either `bpe` or `wpe`')
tokenizer_cfg = OmegaConf.create({'dir': new_tokenizer_dir, 'type': new_tokenizer_type})
# Setup the tokenizer
self._setup_tokenizer(tokenizer_cfg)
# Initialize a dummy vocabulary
vocabulary = self.tokenizer.tokenizer.get_vocab()
# Set the new vocabulary
decoder_config = copy.deepcopy(self.decoder.to_config_dict())
decoder_config.vocabulary = ListConfig(list(vocabulary.keys()))
decoder_num_classes = decoder_config['num_classes']
# Override number of classes if placeholder provided
logging.info(
"\nReplacing old number of classes ({}) with new number of classes - {}".format(
decoder_num_classes, len(vocabulary)
)
)
decoder_config['num_classes'] = len(vocabulary)
del self.decoder
self.decoder = EncDecCTCModelBPE.from_config_dict(decoder_config)
del self.loss
self.loss = CTCLoss(
num_classes=self.decoder.num_classes_with_blank - 1,
zero_infinity=True,
reduction=self._cfg.get("ctc_reduction", "mean_batch"),
)
self._wer = WERBPE(
tokenizer=self.tokenizer,
batch_dim_index=0,
use_cer=self._cfg.get('use_cer', False),
ctc_decode=True,
log_prediction=self._cfg.get("log_prediction", False),
)
# Update config
OmegaConf.set_struct(self._cfg.decoder, False)
self._cfg.decoder = decoder_config
OmegaConf.set_struct(self._cfg.decoder, True)
logging.info(f"Changed tokenizer to {self.decoder.vocabulary} vocabulary.")
@@ -0,0 +1,649 @@
# Copyright (c) 2020, 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 copy
import json
import os
import tempfile
from math import ceil
from typing import Dict, List, Optional, Union
import torch
from omegaconf import DictConfig, OmegaConf, open_dict
from pytorch_lightning import Trainer
from tqdm.auto import tqdm
from nemo.collections.asr.data import audio_to_text_dataset
from nemo.collections.asr.data.audio_to_text_dali import DALIOutputs
from nemo.collections.asr.losses.ctc import CTCLoss
from nemo.collections.asr.metrics.wer import WER
from nemo.collections.asr.models.asr_model import ASRModel, ExportableEncDecModel
from nemo.collections.asr.parts.mixins import ASRModuleMixin
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import AudioSignal, LabelsType, LengthsType, LogprobsType, NeuralType, SpectrogramType
from nemo.utils import logging
__all__ = ['EncDecCTCModel']
class EncDecCTCModel(ASRModel, ExportableEncDecModel, ASRModuleMixin):
"""Base class for encoder decoder CTC-based models."""
@classmethod
def list_available_models(cls) -> Optional[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.
"""
results = []
model = PretrainedModelInfo(
pretrained_model_name="QuartzNet15x5Base-En",
description="QuartzNet15x5 model trained on six datasets: LibriSpeech, Mozilla Common Voice (validated clips from en_1488h_2019-12-10), WSJ, Fisher, Switchboard, and NSC Singapore English. It was trained with Apex/Amp optimization level O1 for 600 epochs. The model achieves a WER of 3.79% on LibriSpeech dev-clean, and a WER of 10.05% on dev-other. Please visit https://ngc.nvidia.com/catalog/models/nvidia:nemospeechmodels for further details.",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemospeechmodels/versions/1.0.0a5/files/QuartzNet15x5Base-En.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_quartznet15x5/versions/1.0.0rc1/files/stt_en_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_zh_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_zh_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_zh_quartznet15x5/versions/1.0.0rc1/files/stt_zh_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_en_jasper10x5dr",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_en_jasper10x5dr",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_en_jasper10x5dr/versions/1.0.0rc1/files/stt_en_jasper10x5dr.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_ca_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_ca_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_ca_quartznet15x5/versions/1.0.0rc1/files/stt_ca_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_it_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_it_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_it_quartznet15x5/versions/1.0.0rc1/files/stt_it_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_fr_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_fr_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_fr_quartznet15x5/versions/1.0.0rc1/files/stt_fr_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_es_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_es_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_es_quartznet15x5/versions/1.0.0rc1/files/stt_es_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_de_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_de_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_de_quartznet15x5/versions/1.0.0rc1/files/stt_de_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_pl_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_pl_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_pl_quartznet15x5/versions/1.0.0rc1/files/stt_pl_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_ru_quartznet15x5",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_ru_quartznet15x5",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_ru_quartznet15x5/versions/1.0.0rc1/files/stt_ru_quartznet15x5.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="stt_zh_citrinet_512",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:stt_zh_citrinet_512",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/stt_zh_citrinet_512/versions/1.0.0rc1/files/stt_zh_citrinet_512.nemo",
)
results.append(model)
model = PretrainedModelInfo(
pretrained_model_name="asr_talknet_aligner",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:asr_talknet_aligner",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/asr_talknet_aligner/versions/1.0.0rc1/files/qn5x5_libri_tts_phonemes.nemo",
)
results.append(model)
return results
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
# Get global rank and total number of GPU workers for IterableDataset partitioning, if applicable
# Global_rank and local_rank is set by LightningModule in Lightning 1.2.0
self.world_size = 1
if trainer is not None:
self.world_size = trainer.num_nodes * trainer.num_gpus
super().__init__(cfg=cfg, trainer=trainer)
self.preprocessor = EncDecCTCModel.from_config_dict(self._cfg.preprocessor)
self.encoder = EncDecCTCModel.from_config_dict(self._cfg.encoder)
with open_dict(self._cfg):
if "feat_in" not in self._cfg.decoder or (
not self._cfg.decoder.feat_in and hasattr(self.encoder, '_feat_out')
):
self._cfg.decoder.feat_in = self.encoder._feat_out
if "feat_in" not in self._cfg.decoder or not self._cfg.decoder.feat_in:
raise ValueError("param feat_in of the decoder's config is not set!")
self.decoder = EncDecCTCModel.from_config_dict(self._cfg.decoder)
self.loss = CTCLoss(
num_classes=self.decoder.num_classes_with_blank - 1,
zero_infinity=True,
reduction=self._cfg.get("ctc_reduction", "mean_batch"),
)
if hasattr(self._cfg, 'spec_augment') and self._cfg.spec_augment is not None:
self.spec_augmentation = EncDecCTCModel.from_config_dict(self._cfg.spec_augment)
else:
self.spec_augmentation = None
# Setup metric objects
self._wer = WER(
vocabulary=self.decoder.vocabulary,
batch_dim_index=0,
use_cer=self._cfg.get('use_cer', False),
ctc_decode=True,
dist_sync_on_step=True,
log_prediction=self._cfg.get("log_prediction", False),
)
@torch.no_grad()
def transcribe(
self,
paths2audio_files: List[str],
batch_size: int = 4,
logprobs: bool = False,
return_hypotheses: bool = False,
) -> List[str]:
"""
Uses greedy decoding to transcribe audio files. Use this method for debugging and prototyping.
Args:
paths2audio_files: (a list) of paths to audio files. \
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.
logprobs: (bool) pass True to get log probabilities instead of transcripts.
return_hypotheses: (bool) Either return hypotheses or text
With hypotheses can do some postprocessing like getting timestamp or rescoring
Returns:
A list of transcriptions (or raw log probabilities if logprobs is True) in the same order as paths2audio_files
"""
if paths2audio_files is None or len(paths2audio_files) == 0:
return {}
if return_hypotheses and logprobs:
raise ValueError(
"Either `return_hypotheses` or `logprobs` can be True at any given time."
"Returned hypotheses will contain the logprobs."
)
# We will store transcriptions here
hypotheses = []
# Model's mode and device
mode = self.training
device = next(self.parameters()).device
dither_value = self.preprocessor.featurizer.dither
pad_to_value = self.preprocessor.featurizer.pad_to
try:
self.preprocessor.featurizer.dither = 0.0
self.preprocessor.featurizer.pad_to = 0
# Switch model to evaluation mode
self.eval()
# Freeze the encoder and decoder modules
self.encoder.freeze()
self.decoder.freeze()
logging_level = logging.get_verbosity()
logging.set_verbosity(logging.WARNING)
# Work in tmp directory - will store manifest file there
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, 'manifest.json'), 'w') as fp:
for audio_file in paths2audio_files:
entry = {'audio_filepath': audio_file, 'duration': 100000, 'text': 'nothing'}
fp.write(json.dumps(entry) + '\n')
config = {'paths2audio_files': paths2audio_files, 'batch_size': batch_size, 'temp_dir': tmpdir}
temporary_datalayer = self._setup_transcribe_dataloader(config)
for test_batch in tqdm(temporary_datalayer, desc="Transcribing"):
logits, logits_len, greedy_predictions = self.forward(
input_signal=test_batch[0].to(device), input_signal_length=test_batch[1].to(device)
)
if logprobs:
# dump log probs per file
for idx in range(logits.shape[0]):
lg = logits[idx][: logits_len[idx]]
hypotheses.append(lg.cpu().numpy())
else:
current_hypotheses = self._wer.ctc_decoder_predictions_tensor(
greedy_predictions, predictions_len=logits_len, return_hypotheses=return_hypotheses,
)
if return_hypotheses:
# dump log probs per file
for idx in range(logits.shape[0]):
current_hypotheses[idx].y_sequence = logits[idx][: logits_len[idx]]
hypotheses += current_hypotheses
del greedy_predictions
del logits
del test_batch
finally:
# set mode back to its original value
self.train(mode=mode)
self.preprocessor.featurizer.dither = dither_value
self.preprocessor.featurizer.pad_to = pad_to_value
if mode is True:
self.encoder.unfreeze()
self.decoder.unfreeze()
logging.set_verbosity(logging_level)
return hypotheses
def change_vocabulary(self, new_vocabulary: List[str]):
"""
Changes vocabulary used during CTC decoding process. Use this method when fine-tuning on from pre-trained model.
This method changes only decoder and leaves encoder and pre-processing modules unchanged. For example, you would
use it if you want to use pretrained encoder when fine-tuning on a data in another language, or when you'd need
model to learn capitalization, punctuation and/or special characters.
If new_vocabulary == self.decoder.vocabulary then nothing will be changed.
Args:
new_vocabulary: list with new vocabulary. Must contain at least 2 elements. Typically, \
this is target alphabet.
Returns: None
"""
if self.decoder.vocabulary == new_vocabulary:
logging.warning(f"Old {self.decoder.vocabulary} and new {new_vocabulary} match. Not changing anything.")
else:
if new_vocabulary is None or len(new_vocabulary) == 0:
raise ValueError(f'New vocabulary must be non-empty list of chars. But I got: {new_vocabulary}')
decoder_config = self.decoder.to_config_dict()
new_decoder_config = copy.deepcopy(decoder_config)
new_decoder_config['vocabulary'] = new_vocabulary
new_decoder_config['num_classes'] = len(new_vocabulary)
del self.decoder
self.decoder = EncDecCTCModel.from_config_dict(new_decoder_config)
del self.loss
self.loss = CTCLoss(
num_classes=self.decoder.num_classes_with_blank - 1,
zero_infinity=True,
reduction=self._cfg.get("ctc_reduction", "mean_batch"),
)
self._wer = WER(
vocabulary=self.decoder.vocabulary,
batch_dim_index=0,
use_cer=self._cfg.get('use_cer', False),
ctc_decode=True,
dist_sync_on_step=True,
log_prediction=self._cfg.get("log_prediction", False),
)
# Update config
OmegaConf.set_struct(self._cfg.decoder, False)
self._cfg.decoder = new_decoder_config
OmegaConf.set_struct(self._cfg.decoder, True)
ds_keys = ['train_ds', 'validation_ds', 'test_ds']
for key in ds_keys:
if key in self.cfg:
with open_dict(self.cfg[key]):
self.cfg[key]['labels'] = OmegaConf.create(new_vocabulary)
logging.info(f"Changed decoder to output to {self.decoder.vocabulary} vocabulary.")
def _setup_dataloader_from_config(self, config: Optional[Dict]):
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
# Automatically inject args from model config to dataloader config
audio_to_text_dataset.inject_dataloader_value_from_model_config(self.cfg, config, key='sample_rate')
audio_to_text_dataset.inject_dataloader_value_from_model_config(self.cfg, config, key='labels')
shuffle = config['shuffle']
device = 'gpu' if torch.cuda.is_available() else 'cpu'
if config.get('use_dali', False):
device_id = self.local_rank if device == 'gpu' else None
dataset = audio_to_text_dataset.get_dali_char_dataset(
config=config,
shuffle=shuffle,
device_id=device_id,
global_rank=self.global_rank,
world_size=self.world_size,
preprocessor_cfg=self._cfg.preprocessor,
)
return dataset
# Instantiate tarred dataset loader or normal dataset loader
if config.get('is_tarred', False):
if ('tarred_audio_filepaths' in config and config['tarred_audio_filepaths'] is None) or (
'manifest_filepath' in config and config['manifest_filepath'] is None
):
logging.warning(
"Could not load dataset as `manifest_filepath` was None or "
f"`tarred_audio_filepaths` is None. Provided config : {config}"
)
return None
shuffle_n = config.get('shuffle_n', 4 * config['batch_size']) if shuffle else 0
dataset = audio_to_text_dataset.get_tarred_char_dataset(
config=config,
shuffle_n=shuffle_n,
global_rank=self.global_rank,
world_size=self.world_size,
augmentor=augmentor,
)
shuffle = False
else:
if 'manifest_filepath' in config and config['manifest_filepath'] is None:
logging.warning(f"Could not load dataset as `manifest_filepath` was None. Provided config : {config}")
return None
dataset = audio_to_text_dataset.get_char_dataset(config=config, augmentor=augmentor)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=config['batch_size'],
collate_fn=dataset.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_training_data(self, train_data_config: Optional[Union[DictConfig, Dict]]):
"""
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.AudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.AudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text_dali.AudioToCharDALIDataset`
"""
if 'shuffle' not in train_data_config:
train_data_config['shuffle'] = True
# preserve config
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 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'])
)
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.AudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.AudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text_dali.AudioToCharDALIDataset`
"""
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)
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.AudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.AudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text_dali.AudioToCharDALIDataset`
"""
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)
@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),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"outputs": NeuralType(('B', 'T', 'D'), LogprobsType()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
"greedy_predictions": NeuralType(('B', 'T'), LabelsType()),
}
@typecheck()
def forward(
self, input_signal=None, input_signal_length=None, processed_signal=None, processed_signal_length=None
):
"""
Forward pass of the 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.
Returns:
A tuple of 3 elements -
1) The log probabilities tensor of shape [B, T, D].
2) The lengths of the acoustic sequence after propagation through the encoder, of shape [B].
3) The greedy token predictions of the model of shape [B, T] (via argmax)
"""
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) == 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,
)
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)
log_probs = self.decoder(encoder_output=encoded)
greedy_predictions = log_probs.argmax(dim=-1, keepdim=False)
return log_probs, encoded_len, greedy_predictions
# PTL-specific methods
def training_step(self, batch, batch_nb):
signal, signal_len, transcript, transcript_len = batch
if isinstance(batch, DALIOutputs) and batch.has_processed_signal:
log_probs, encoded_len, predictions = self.forward(
processed_signal=signal, processed_signal_length=signal_len
)
else:
log_probs, encoded_len, predictions = self.forward(input_signal=signal, input_signal_length=signal_len)
loss_value = self.loss(
log_probs=log_probs, targets=transcript, input_lengths=encoded_len, target_lengths=transcript_len
)
tensorboard_logs = {'train_loss': loss_value, 'learning_rate': self._optimizer.param_groups[0]['lr']}
if hasattr(self, '_trainer') and self._trainer is not None:
log_every_n_steps = self._trainer.log_every_n_steps
else:
log_every_n_steps = 1
if (batch_nb + 1) % log_every_n_steps == 0:
self._wer.update(
predictions=predictions,
targets=transcript,
target_lengths=transcript_len,
predictions_lengths=encoded_len,
)
wer, _, _ = self._wer.compute()
self._wer.reset()
tensorboard_logs.update({'training_batch_wer': wer})
return {'loss': loss_value, 'log': tensorboard_logs}
def validation_step(self, batch, batch_idx, dataloader_idx=0):
signal, signal_len, transcript, transcript_len = batch
if isinstance(batch, DALIOutputs) and batch.has_processed_signal:
log_probs, encoded_len, predictions = self.forward(
processed_signal=signal, processed_signal_length=signal_len
)
else:
log_probs, encoded_len, predictions = self.forward(input_signal=signal, input_signal_length=signal_len)
loss_value = self.loss(
log_probs=log_probs, targets=transcript, input_lengths=encoded_len, target_lengths=transcript_len
)
self._wer.update(
predictions=predictions, targets=transcript, target_lengths=transcript_len, predictions_lengths=encoded_len
)
wer, wer_num, wer_denom = self._wer.compute()
self._wer.reset()
return {
'val_loss': loss_value,
'val_wer_num': wer_num,
'val_wer_denom': wer_denom,
'val_wer': wer,
}
def test_step(self, batch, batch_idx, dataloader_idx=0):
logs = self.validation_step(batch, batch_idx, dataloader_idx=dataloader_idx)
test_logs = {
'test_loss': logs['val_loss'],
'test_wer_num': logs['val_wer_num'],
'test_wer_denom': logs['val_wer_denom'],
'test_wer': logs['val_wer'],
}
return test_logs
def test_dataloader(self):
if self._test_dl is not None:
return self._test_dl
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.
Returns:
A pytorch DataLoader for the given audio file(s).
"""
dl_config = {
'manifest_filepath': os.path.join(config['temp_dir'], 'manifest.json'),
'sample_rate': self.preprocessor._sample_rate,
'labels': self.decoder.vocabulary,
'batch_size': min(config['batch_size'], len(config['paths2audio_files'])),
'trim_silence': True,
'shuffle': False,
}
temporary_datalayer = self._setup_dataloader_from_config(config=DictConfig(dl_config))
return temporary_datalayer
@@ -0,0 +1,409 @@
# Copyright (c) 2020, 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 copy
import json
import os
import pickle as pkl
from typing import Dict, List, Optional, Union
import librosa
import torch
from omegaconf import DictConfig
from omegaconf.omegaconf import open_dict
from pytorch_lightning import Trainer
from nemo.collections.asr.data.audio_to_label import AudioToSpeechLabelDataset
from nemo.collections.asr.losses.angularloss import AngularSoftmaxLoss
from nemo.collections.asr.models.asr_model import ExportableEncDecModel
from nemo.collections.asr.parts.preprocessing.features import WaveformFeaturizer
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.collections.common.losses import CrossEntropyLoss as CELoss
from nemo.collections.common.metrics import TopKClassificationAccuracy
from nemo.core.classes import ModelPT
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import *
from nemo.utils import logging
__all__ = ['EncDecSpeakerLabelModel', 'ExtractSpeakerEmbeddingsModel']
class EncDecSpeakerLabelModel(ModelPT, ExportableEncDecModel):
"""Encoder decoder class for speaker label models.
Model class creates training, validation methods for setting up data
performing model forward pass.
Expects config dict for
* preprocessor
* Jasper/Quartznet Encoder
* Speaker Decoder
"""
@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.
"""
result = []
model = PretrainedModelInfo(
pretrained_model_name="speakerrecognition_speakernet",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/speakerrecognition_speakernet/versions/1.0.0rc1/files/speakerrecognition_speakernet.nemo",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:speakerrecognition_speakernet",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="speakerverification_speakernet",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/speakerverification_speakernet/versions/1.0.0rc1/files/speakerverification_speakernet.nemo",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:speakerverification_speakernet",
)
result.append(model)
model = PretrainedModelInfo(
pretrained_model_name="speakerdiarization_speakernet",
location="https://api.ngc.nvidia.com/v2/models/nvidia/nemo/speakerdiarization_speakernet/versions/1.0.0rc1/files/speakerdiarization_speakernet.nemo",
description="For details about this model, please visit https://ngc.nvidia.com/catalog/models/nvidia:nemo:speakerdiarization_speakernet",
)
result.append(model)
return result
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
super().__init__(cfg=cfg, trainer=trainer)
self.preprocessor = EncDecSpeakerLabelModel.from_config_dict(cfg.preprocessor)
self.encoder = EncDecSpeakerLabelModel.from_config_dict(cfg.encoder)
self.decoder = EncDecSpeakerLabelModel.from_config_dict(cfg.decoder)
if 'angular' in cfg.decoder and cfg.decoder['angular']:
logging.info("Training with Angular Softmax Loss")
scale = cfg.loss.scale
margin = cfg.loss.margin
self.loss = AngularSoftmaxLoss(scale=scale, margin=margin)
else:
logging.info("Training with Softmax-CrossEntropy loss")
self.loss = CELoss()
self.task = None
self._accuracy = TopKClassificationAccuracy(top_k=[1])
def __setup_dataloader_from_config(self, config: Optional[Dict]):
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
featurizer = WaveformFeaturizer(
sample_rate=config['sample_rate'], int_values=config.get('int_values', False), augmentor=augmentor
)
self.dataset = AudioToSpeechLabelDataset(
manifest_filepath=config['manifest_filepath'],
labels=config['labels'],
featurizer=featurizer,
max_duration=config.get('max_duration', None),
min_duration=config.get('min_duration', None),
trim=False,
time_length=config.get('time_length', 8),
shift_length=config.get('shift_length', 0.75),
)
if self.task == 'diarization':
logging.info("Setting up diarization parameters")
_collate_func = self.dataset.sliced_seq_collate_fn
batch_size = 1
shuffle = False
else:
logging.info("Setting up identification parameters")
_collate_func = self.dataset.fixed_seq_collate_fn
batch_size = config['batch_size']
shuffle = config.get('shuffle', False)
return torch.utils.data.DataLoader(
dataset=self.dataset,
batch_size=batch_size,
collate_fn=_collate_func,
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_training_data(self, train_data_layer_config: Optional[Union[DictConfig, Dict]]):
if 'shuffle' not in train_data_layer_config:
train_data_layer_config['shuffle'] = True
self.task = 'identification'
self._train_dl = self.__setup_dataloader_from_config(config=train_data_layer_config)
def setup_validation_data(self, val_data_layer_config: Optional[Union[DictConfig, Dict]]):
val_data_layer_config['labels'] = self.dataset.labels
self.task = 'identification'
self._validation_dl = self.__setup_dataloader_from_config(config=val_data_layer_config)
def setup_test_data(self, test_data_layer_params: Optional[Union[DictConfig, Dict]]):
if hasattr(self, 'dataset'):
test_data_layer_params['labels'] = self.dataset.labels
if 'task' in test_data_layer_params and test_data_layer_params['task']:
self.task = test_data_layer_params['task'].lower()
self.time_length = test_data_layer_params.get('time_length', 1.5)
self.shift_length = test_data_layer_params.get('shift_length', 0.75)
else:
self.task = 'identification'
self.embedding_dir = test_data_layer_params.get('embedding_dir', './')
self._test_dl = self.__setup_dataloader_from_config(config=test_data_layer_params)
self.test_manifest = test_data_layer_params.get('manifest_filepath', None)
def test_dataloader(self):
if self._test_dl is not None:
return self._test_dl
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
if hasattr(self.preprocessor, '_sample_rate'):
audio_eltype = AudioSignal(freq=self.preprocessor._sample_rate)
else:
audio_eltype = AudioSignal()
return {
"input_signal": NeuralType(('B', 'T'), audio_eltype),
"input_signal_length": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"logits": NeuralType(('B', 'D'), LogitsType()),
"embs": NeuralType(('B', 'D'), AcousticEncodedRepresentation()),
}
@typecheck()
def forward(self, input_signal, input_signal_length):
processed_signal, processed_signal_len = self.preprocessor(
input_signal=input_signal, length=input_signal_length,
)
encoded, _ = self.encoder(audio_signal=processed_signal, length=processed_signal_len)
logits, embs = self.decoder(encoder_output=encoded)
return logits, embs
# PTL-specific methods
def training_step(self, batch, batch_idx):
audio_signal, audio_signal_len, labels, _ = batch
logits, _ = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss = self.loss(logits=logits, labels=labels)
self.log('loss', loss)
self.log('learning_rate', self._optimizer.param_groups[0]['lr'])
self._accuracy(logits=logits, labels=labels)
top_k = self._accuracy.compute()
self._accuracy.reset()
for i, top_i in enumerate(top_k):
self.log(f'training_batch_accuracy_top@{i}', top_i)
return {'loss': loss}
def validation_step(self, batch, batch_idx, dataloader_idx: int = 0):
audio_signal, audio_signal_len, labels, _ = batch
logits, _ = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss_value = self.loss(logits=logits, labels=labels)
acc_top_k = self._accuracy(logits=logits, labels=labels)
correct_counts, total_counts = self._accuracy.correct_counts_k, self._accuracy.total_counts_k
return {
'val_loss': loss_value,
'val_correct_counts': correct_counts,
'val_total_counts': total_counts,
'val_acc_top_k': acc_top_k,
}
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
val_loss_mean = torch.stack([x['val_loss'] for x in outputs]).mean()
correct_counts = torch.stack([x['val_correct_counts'] for x in outputs]).sum(axis=0)
total_counts = torch.stack([x['val_total_counts'] for x in outputs]).sum(axis=0)
self._accuracy.correct_counts_k = correct_counts
self._accuracy.total_counts_k = total_counts
topk_scores = self._accuracy.compute()
self._accuracy.reset()
logging.info("val_loss: {:.3f}".format(val_loss_mean))
self.log('val_loss', val_loss_mean)
for top_k, score in zip(self._accuracy.top_k, topk_scores):
self.log('val_epoch_accuracy_top@{}'.format(top_k), score)
return {
'val_loss': val_loss_mean,
'val_acc_top_k': topk_scores,
}
def test_step(self, batch, batch_idx, dataloader_idx: int = 0):
audio_signal, audio_signal_len, labels, _ = batch
logits, _ = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
loss_value = self.loss(logits=logits, labels=labels)
acc_top_k = self._accuracy(logits=logits, labels=labels)
correct_counts, total_counts = self._accuracy.correct_counts_k, self._accuracy.total_counts_k
return {
'test_loss': loss_value,
'test_correct_counts': correct_counts,
'test_total_counts': total_counts,
'test_acc_top_k': acc_top_k,
}
def multi_test_epoch_end(self, outputs, dataloader_idx: int = 0):
test_loss_mean = torch.stack([x['test_loss'] for x in outputs]).mean()
correct_counts = torch.stack([x['test_correct_counts'] for x in outputs]).sum(axis=0)
total_counts = torch.stack([x['test_total_counts'] for x in outputs]).sum(axis=0)
self._accuracy.correct_counts_k = correct_counts
self._accuracy.total_counts_k = total_counts
topk_scores = self._accuracy.compute()
self._accuracy.reset()
logging.info("test_loss: {:.3f}".format(test_loss_mean))
self.log('test_loss', test_loss_mean)
for top_k, score in zip(self._accuracy.top_k, topk_scores):
self.log('test_epoch_accuracy_top@{}'.format(top_k), score)
return {
'test_loss': test_loss_mean,
'test_acc_top_k': topk_scores,
}
def setup_finetune_model(self, model_config: DictConfig):
"""
setup_finetune_model method sets up training data, validation data and test data with new
provided config, this checks for the previous labels set up during training from scratch, if None,
it sets up labels for provided finetune data from manifest files
Args:
model_config: cfg which has train_ds, optional validation_ds, optional test_ds and
mandatory encoder and decoder model params
make sure you set num_classes correctly for finetune data
Returns: None
"""
if hasattr(self, 'dataset'):
scratch_labels = self.dataset.labels
else:
scratch_labels = None
logging.info("Setting up data loaders with manifests provided from model_config")
if 'train_ds' in model_config and model_config.train_ds is not None:
self.setup_training_data(model_config.train_ds)
else:
raise KeyError("train_ds is not found in model_config but you need it for fine tuning")
if self.dataset.labels is None or len(self.dataset.labels) == 0:
raise ValueError(f'New labels must be non-empty list of labels. But I got: {self.dataset.labels}')
if 'validation_ds' in model_config and model_config.validation_ds is not None:
self.setup_multiple_validation_data(model_config.validation_ds)
if 'test_ds' in model_config and model_config.test_ds is not None:
self.setup_multiple_test_data(model_config.test_ds)
if scratch_labels == self.dataset.labels: # checking for new finetune dataset labels
logging.warning(
"Trained dataset labels are same as finetune dataset labels -- continuing change of decoder parameters"
)
elif scratch_labels is None:
logging.warning(
"Either you provided a dummy manifest file during training from scratch or you restored from a pretrained nemo file"
)
decoder_config = model_config.decoder
new_decoder_config = copy.deepcopy(decoder_config)
if new_decoder_config['num_classes'] != len(self.dataset.labels):
raise ValueError(
"number of classes provided {} is not same as number of different labels in finetuning data: {}".format(
new_decoder_config['num_classes'], len(self.dataset.labels)
)
)
del self.decoder
self.decoder = EncDecSpeakerLabelModel.from_config_dict(new_decoder_config)
with open_dict(self._cfg.decoder):
self._cfg.decoder = new_decoder_config
logging.info(f"Changed decoder output to # {self.decoder._num_classes} classes.")
@torch.no_grad()
def get_embedding(self, path2audio_file):
audio, sr = librosa.load(path2audio_file, sr=None)
target_sr = self._cfg.train_ds.get('sample_rate', 16000)
if sr != target_sr:
audio = librosa.core.resample(audio, sr, target_sr)
audio_length = audio.shape[0]
device = self.device
audio_signal, audio_signal_len = (
torch.tensor([audio], device=device),
torch.tensor([audio_length], device=device),
)
mode = self.training
self.freeze()
_, embs = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
self.train(mode=mode)
if mode is True:
self.unfreeze()
del audio_signal, audio_signal_len
return embs
class ExtractSpeakerEmbeddingsModel(EncDecSpeakerLabelModel):
"""
This Model class facilitates extraction of speaker embeddings from a pretrained model.
Respective embedding file is saved in self.embedding dir passed through cfg
"""
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
super().__init__(cfg=cfg, trainer=trainer)
def test_step(self, batch, batch_ix):
audio_signal, audio_signal_len, labels, slices = batch
_, embs = self.forward(input_signal=audio_signal, input_signal_length=audio_signal_len)
return {'embs': embs, 'labels': labels, 'slices': slices}
def test_epoch_end(self, outputs):
embs = torch.cat([x['embs'] for x in outputs])
slices = torch.cat([x['slices'] for x in outputs])
emb_shape = embs.shape[-1]
embs = embs.view(-1, emb_shape).cpu().numpy()
out_embeddings = {}
start_idx = 0
with open(self.test_manifest, 'r') as manifest:
for idx, line in enumerate(manifest.readlines()):
line = line.strip()
dic = json.loads(line)
structure = dic['audio_filepath'].split('/')[-3:]
uniq_name = '@'.join(structure)
if uniq_name in out_embeddings:
raise KeyError("Embeddings for label {} already present in emb dictionary".format(uniq_name))
num_slices = slices[idx]
end_idx = start_idx + num_slices
out_embeddings[uniq_name] = embs[start_idx:end_idx].mean(axis=0)
start_idx = end_idx
embedding_dir = os.path.join(self.embedding_dir, 'embeddings')
if not os.path.exists(embedding_dir):
os.mkdir(embedding_dir)
prefix = self.test_manifest.split('/')[-1].split('.')[-2]
name = os.path.join(embedding_dir, prefix)
pkl.dump(out_embeddings, open(name + '_embeddings.pkl', 'wb'))
logging.info("Saved embedding files to {}".format(embedding_dir))
return {}
@@ -0,0 +1,290 @@
# Copyright (c) 2020, 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 copy
import os
from typing import Dict, Optional
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.losses.rnnt import RNNTLoss
from nemo.collections.asr.metrics.rnnt_wer_bpe import RNNTBPEWER, RNNTBPEDecoding
from nemo.collections.asr.models.rnnt_models import EncDecRNNTModel
from nemo.collections.asr.parts.mixins import ASRBPEMixin
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.core.classes.common import PretrainedModelInfo
from nemo.utils import logging, model_utils
class EncDecRNNTBPEModel(EncDecRNNTModel, ASRBPEMixin):
"""Base class for encoder decoder RNNT-based models with subword tokenization."""
@classmethod
def list_available_models(cls) -> Optional[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.
"""
result = []
return result
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
super().__init__(cfg=cfg, trainer=trainer)
# 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 = RNNTBPEWER(
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 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)
def change_vocabulary(
self, new_tokenizer_dir: str, new_tokenizer_type: str, decoding_cfg: Optional[DictConfig] = None
):
"""
Changes vocabulary used during RNNT decoding process. Use this method when fine-tuning on from pre-trained model.
This method changes only decoder and leaves encoder and pre-processing modules unchanged. For example, you would
use it if you want to use pretrained encoder when fine-tuning on data in another language, or when you'd need
model to learn capitalization, punctuation and/or special characters.
Args:
new_tokenizer_dir: Directory path to tokenizer.
new_tokenizer_type: Type of tokenizer. Can be either `bpe` or `wpe`.
decoding_cfg: A config for the decoder, which is optional. If the decoding type
needs to be changed (from say Greedy to Beam decoding etc), the config can be passed here.
Returns: None
"""
if not os.path.isdir(new_tokenizer_dir):
raise NotADirectoryError(
f'New tokenizer dir must be non-empty path to a directory. But I got: {new_tokenizer_dir}'
)
if new_tokenizer_type.lower() not in ('bpe', 'wpe'):
raise ValueError(f'New tokenizer type must be either `bpe` or `wpe`')
tokenizer_cfg = OmegaConf.create({'dir': new_tokenizer_dir, 'type': new_tokenizer_type})
# Setup the tokenizer
self._setup_tokenizer(tokenizer_cfg)
# Initialize a dummy vocabulary
vocabulary = self.tokenizer.tokenizer.get_vocab()
joint_config = self.joint.to_config_dict()
new_joint_config = copy.deepcopy(joint_config)
new_joint_config['vocabulary'] = ListConfig(list(vocabulary.keys()))
new_joint_config['num_classes'] = len(vocabulary)
del self.joint
self.joint = EncDecRNNTBPEModel.from_config_dict(new_joint_config)
decoder_config = self.decoder.to_config_dict()
new_decoder_config = copy.deepcopy(decoder_config)
new_decoder_config.vocab_size = len(vocabulary)
del self.decoder
self.decoder = EncDecRNNTBPEModel.from_config_dict(new_decoder_config)
del self.loss
self.loss = RNNTLoss(num_classes=self.joint.num_classes_with_blank - 1)
if decoding_cfg is None:
# Assume same decoding config as before
decoding_cfg = self.cfg.decoding
self.decoding = RNNTBPEDecoding(
decoding_cfg=decoding_cfg, decoder=self.decoder, joint=self.joint, tokenizer=self.tokenizer,
)
self.wer = RNNTBPEWER(
decoding=self.decoding,
batch_dim_index=self.wer.batch_dim_index,
use_cer=self.wer.use_cer,
log_prediction=self.wer.log_prediction,
dist_sync_on_step=True,
)
# Setup fused Joint step
if self.joint.fuse_loss_wer:
self.joint.set_loss(self.loss)
self.joint.set_wer(self.wer)
# Update config
with open_dict(self.cfg.joint):
self.cfg.joint = new_joint_config
with open_dict(self.cfg.decoder):
self.cfg.decoder = new_decoder_config
with open_dict(self.cfg.decoding):
self.cfg.decoding = decoding_cfg
logging.info(f"Changed decoder to output to {self.joint.vocabulary} vocabulary.")
def change_decoding_strategy(self, decoding_cfg: DictConfig):
"""
Changes decoding strategy used during RNNT decoding process.
Args:
decoding_cfg: A config for the decoder, which is optional. If the decoding type
needs to be changed (from say Greedy to Beam decoding etc), the config can be passed here.
"""
if decoding_cfg is None:
# Assume same decoding config as before
logging.info("No `decoding_cfg` passed when changing decoding strategy, using internal config")
decoding_cfg = self.cfg.decoding
self.decoding = RNNTBPEDecoding(
decoding_cfg=decoding_cfg, decoder=self.decoder, joint=self.joint, tokenizer=self.tokenizer,
)
self.wer = RNNTBPEWER(
decoding=self.decoding,
batch_dim_index=self.wer.batch_dim_index,
use_cer=self.wer.use_cer,
log_prediction=self.wer.log_prediction,
dist_sync_on_step=True,
)
# Setup fused Joint step
if self.joint.fuse_loss_wer:
self.joint.set_loss(self.loss)
self.joint.set_wer(self.wer)
# Update config
with open_dict(self.cfg.decoding):
self.cfg.decoding = decoding_cfg
logging.info(f"Changed decoding strategy to \n{OmegaConf.to_yaml(self.cfg.decoding)}")
def _setup_dataloader_from_config(self, config: Optional[Dict]):
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
shuffle = config['shuffle']
# Instantiate tarred dataset loader or normal dataset loader
if config.get('is_tarred', False):
if ('tarred_audio_filepaths' in config and config['tarred_audio_filepaths'] is None) or (
'manifest_filepath' in config and config['manifest_filepath'] is None
):
logging.warning(
"Could not load dataset as `manifest_filepath` was None or "
f"`tarred_audio_filepaths` is None. Provided config : {config}"
)
return None
shuffle_n = config.get('shuffle_n', 4 * config['batch_size']) if shuffle else 0
dataset = audio_to_text_dataset.get_tarred_bpe_dataset(
config=config,
tokenizer=self.tokenizer,
shuffle_n=shuffle_n,
global_rank=self.global_rank,
world_size=self.world_size,
augmentor=augmentor,
)
shuffle = False
else:
if 'manifest_filepath' in config and config['manifest_filepath'] is None:
logging.warning(f"Could not load dataset as `manifest_filepath` was None. Provided config : {config}")
return None
dataset = audio_to_text_dataset.get_bpe_dataset(
config=config, tokenizer=self.tokenizer, augmentor=augmentor
)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=config['batch_size'],
collate_fn=dataset.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.
Returns:
A pytorch DataLoader for the given audio file(s).
"""
dl_config = {
'manifest_filepath': os.path.join(config['temp_dir'], 'manifest.json'),
'sample_rate': self.preprocessor._sample_rate,
'batch_size': min(config['batch_size'], len(config['paths2audio_files'])),
'shuffle': False,
}
temporary_datalayer = self._setup_dataloader_from_config(config=DictConfig(dl_config))
return temporary_datalayer
@@ -0,0 +1,835 @@
# Copyright (c) 2020, 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 copy
import json
import os
import tempfile
from math import ceil
from typing import Dict, List, Optional, Union
import torch
from omegaconf import DictConfig, OmegaConf, open_dict
from pytorch_lightning import Trainer
from tqdm.auto import tqdm
from nemo.collections.asr.data import audio_to_text_dataset
from nemo.collections.asr.data.audio_to_text_dali import DALIOutputs
from nemo.collections.asr.losses.rnnt import RNNTLoss, resolve_rnnt_default_loss_name
from nemo.collections.asr.metrics.rnnt_wer import RNNTWER, RNNTDecoding
from nemo.collections.asr.models.asr_model import ASRModel
from nemo.collections.asr.parts.mixins import ASRModuleMixin
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import AcousticEncodedRepresentation, AudioSignal, LengthsType, NeuralType, SpectrogramType
from nemo.utils import logging
class EncDecRNNTModel(ASRModel, ASRModuleMixin):
"""Base class for encoder decoder RNNT-based models."""
@classmethod
def list_available_models(cls) -> Optional[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.
"""
result = []
return result
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
# Get global rank and total number of GPU workers for IterableDataset partitioning, if applicable
# Global_rank and local_rank is set by LightningModule in Lightning 1.2.0
self.world_size = 1
if trainer is not None:
self.world_size = trainer.num_nodes * trainer.num_gpus
super().__init__(cfg=cfg, trainer=trainer)
# Initialize components
self.preprocessor = EncDecRNNTModel.from_config_dict(self.cfg.preprocessor)
self.encoder = EncDecRNNTModel.from_config_dict(self.cfg.encoder)
# Update config values required by components dynamically
with open_dict(self.cfg.decoder):
self.cfg.decoder.vocab_size = len(self.cfg.labels)
with open_dict(self.cfg.joint):
self.cfg.joint.num_classes = len(self.cfg.labels)
self.cfg.joint.vocabulary = self.cfg.labels
self.cfg.joint.jointnet.encoder_hidden = self.cfg.model_defaults.enc_hidden
self.cfg.joint.jointnet.pred_hidden = self.cfg.model_defaults.pred_hidden
self.decoder = EncDecRNNTModel.from_config_dict(self.cfg.decoder)
self.joint = EncDecRNNTModel.from_config_dict(self.cfg.joint)
# Setup RNNT Loss
loss_name, loss_kwargs = self.extract_rnnt_loss_cfg(self.cfg.get("loss", None))
self.loss = RNNTLoss(
num_classes=self.joint.num_classes_with_blank - 1, loss_name=loss_name, loss_kwargs=loss_kwargs
)
if hasattr(self.cfg, 'spec_augment') and self._cfg.spec_augment is not None:
self.spec_augmentation = EncDecRNNTModel.from_config_dict(self.cfg.spec_augment)
else:
self.spec_augmentation = None
# Setup decoding objects
self.decoding = RNNTDecoding(
decoding_cfg=self.cfg.decoding, decoder=self.decoder, joint=self.joint, vocabulary=self.joint.vocabulary,
)
# Setup WER calculation
self.wer = RNNTWER(
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,
)
# Whether to compute loss during evaluation
if 'compute_eval_loss' in self.cfg:
self.compute_eval_loss = self.cfg.compute_eval_loss
else:
self.compute_eval_loss = 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)
self.setup_optim_normalization()
def setup_optim_normalization(self):
"""
Helper method to setup normalization of certain parts of the model prior to the optimization step.
Supported pre-optimization normalizations are as follows:
.. code-block:: yaml
# Variation Noise injection
model:
variational_noise:
std: 0.0
start_step: 0
# Joint - Length normalization
model:
normalize_joint_txu: false
# Encoder Network - gradient normalization
model:
normalize_encoder_norm: false
# Decoder / Prediction Network - gradient normalization
model:
normalize_decoder_norm: false
# Joint - gradient normalization
model:
normalize_joint_norm: false
"""
# setting up the variational noise for the decoder
if hasattr(self.cfg, 'variational_noise'):
self._optim_variational_noise_std = self.cfg['variational_noise'].get('std', 0)
self._optim_variational_noise_start = self.cfg['variational_noise'].get('start_step', 0)
else:
self._optim_variational_noise_std = 0
self._optim_variational_noise_start = 0
# Setup normalized gradients for model joint by T x U scaling factor (joint length normalization)
self._optim_normalize_joint_txu = self.cfg.get('normalize_joint_txu', False)
self._optim_normalize_txu = None
# Setup normalized encoder norm for model
self._optim_normalize_encoder_norm = self.cfg.get('normalize_encoder_norm', False)
# Setup normalized decoder norm for model
self._optim_normalize_decoder_norm = self.cfg.get('normalize_decoder_norm', False)
# Setup normalized joint norm for model
self._optim_normalize_joint_norm = self.cfg.get('normalize_joint_norm', False)
def extract_rnnt_loss_cfg(self, cfg: Optional[DictConfig]):
"""
Helper method to extract the rnnt loss name, and potentially its kwargs
to be passed.
Args:
cfg: Should contain `loss_name` as a string which is resolved to a RNNT loss name.
If the default should be used, then `default` can be used.
Optionally, one can pass additional kwargs to the loss function. The subdict
should have a keyname as follows : `{loss_name}_kwargs`.
Note that whichever loss_name is selected, that corresponding kwargs will be
selected. For the "default" case, the "{resolved_default}_kwargs" will be used.
Examples:
.. code-block:: yaml
loss_name: "default"
warprnnt_numba_kwargs:
kwargs2: some_other_val
Returns:
A tuple, the resolved loss name as well as its kwargs (if found).
"""
if cfg is None:
cfg = DictConfig({})
loss_name = cfg.get("loss_name", "default")
if loss_name == "default":
loss_name = resolve_rnnt_default_loss_name()
loss_kwargs = cfg.get(f"{loss_name}_kwargs", None)
logging.info(f"Using RNNT Loss : {loss_name}\n" f"Loss {loss_name}_kwargs: {loss_kwargs}")
return loss_name, loss_kwargs
@torch.no_grad()
def transcribe(
self, paths2audio_files: List[str], batch_size: int = 4, return_hypotheses: bool = False
) -> List[str]:
"""
Uses greedy decoding to transcribe audio files. Use this method for debugging and prototyping.
Args:
paths2audio_files: (a list) of paths to audio files. \
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
Returns:
A list of transcriptions in the same order as paths2audio_files
"""
if paths2audio_files is None or len(paths2audio_files) == 0:
return {}
# We will store transcriptions here
hypotheses = []
# Model's mode and device
mode = self.training
device = next(self.parameters()).device
try:
# Switch model to evaluation mode
self.eval()
# Freeze the encoder and decoder modules
self.encoder.freeze()
self.decoder.freeze()
self.joint.freeze()
logging_level = logging.get_verbosity()
logging.set_verbosity(logging.WARNING)
# Work in tmp directory - will store manifest file there
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, 'manifest.json'), 'w') as fp:
for audio_file in paths2audio_files:
entry = {'audio_filepath': audio_file, 'duration': 100000, 'text': 'nothing'}
fp.write(json.dumps(entry) + '\n')
config = {'paths2audio_files': paths2audio_files, 'batch_size': batch_size, 'temp_dir': tmpdir}
temporary_datalayer = self._setup_transcribe_dataloader(config)
for test_batch in tqdm(temporary_datalayer, desc="Transcribing"):
encoded, encoded_len = self.forward(
input_signal=test_batch[0].to(device), input_signal_length=test_batch[1].to(device)
)
hypotheses += self.decoding.rnnt_decoder_predictions_tensor(
encoded, encoded_len, return_hypotheses=return_hypotheses
)
del encoded
del test_batch
finally:
# set mode back to its original value
self.train(mode=mode)
logging.set_verbosity(logging_level)
if mode is True:
self.encoder.unfreeze()
self.decoder.unfreeze()
self.joint.unfreeze()
return hypotheses
def change_vocabulary(self, new_vocabulary: List[str], decoding_cfg: Optional[DictConfig] = None):
"""
Changes vocabulary used during RNNT decoding process. Use this method when fine-tuning a pre-trained model.
This method changes only decoder and leaves encoder and pre-processing modules unchanged. For example, you would
use it if you want to use pretrained encoder when fine-tuning on data in another language, or when you'd need
model to learn capitalization, punctuation and/or special characters.
Args:
new_vocabulary: list with new vocabulary. Must contain at least 2 elements. Typically, \
this is target alphabet.
decoding_cfg: A config for the decoder, which is optional. If the decoding type
needs to be changed (from say Greedy to Beam decoding etc), the config can be passed here.
Returns: None
"""
if self.joint.vocabulary == new_vocabulary:
logging.warning(f"Old {self.joint.vocabulary} and new {new_vocabulary} match. Not changing anything.")
else:
if new_vocabulary is None or len(new_vocabulary) == 0:
raise ValueError(f'New vocabulary must be non-empty list of chars. But I got: {new_vocabulary}')
joint_config = self.joint.to_config_dict()
new_joint_config = copy.deepcopy(joint_config)
new_joint_config['vocabulary'] = new_vocabulary
new_joint_config['num_classes'] = len(new_vocabulary)
del self.joint
self.joint = EncDecRNNTModel.from_config_dict(new_joint_config)
decoder_config = self.decoder.to_config_dict()
new_decoder_config = copy.deepcopy(decoder_config)
new_decoder_config.vocab_size = len(new_vocabulary)
del self.decoder
self.decoder = EncDecRNNTModel.from_config_dict(new_decoder_config)
del self.loss
loss_name, loss_kwargs = self.extract_rnnt_loss_cfg(self.cfg.get('loss', None))
self.loss = RNNTLoss(
num_classes=self.joint.num_classes_with_blank - 1, loss_name=loss_name, loss_kwargs=loss_kwargs
)
if decoding_cfg is None:
# Assume same decoding config as before
decoding_cfg = self.cfg.decoding
self.decoding = RNNTDecoding(
decoding_cfg=decoding_cfg, decoder=self.decoder, joint=self.joint, vocabulary=self.joint.vocabulary,
)
self.wer = RNNTWER(
decoding=self.decoding,
batch_dim_index=self.wer.batch_dim_index,
use_cer=self.wer.use_cer,
log_prediction=self.wer.log_prediction,
dist_sync_on_step=True,
)
# Setup fused Joint step
if self.joint.fuse_loss_wer:
self.joint.set_loss(self.loss)
self.joint.set_wer(self.wer)
# Update config
with open_dict(self.cfg.joint):
self.cfg.joint = new_joint_config
with open_dict(self.cfg.decoder):
self.cfg.decoder = new_decoder_config
with open_dict(self.cfg.decoding):
self.cfg.decoding = decoding_cfg
ds_keys = ['train_ds', 'validation_ds', 'test_ds']
for key in ds_keys:
if key in self.cfg:
with open_dict(self.cfg[key]):
self.cfg[key]['labels'] = OmegaConf.create(new_vocabulary)
logging.info(f"Changed decoder to output to {self.joint.vocabulary} vocabulary.")
def change_decoding_strategy(self, decoding_cfg: DictConfig):
"""
Changes decoding strategy used during RNNT decoding process.
Args:
decoding_cfg: A config for the decoder, which is optional. If the decoding type
needs to be changed (from say Greedy to Beam decoding etc), the config can be passed here.
"""
if decoding_cfg is None:
# Assume same decoding config as before
logging.info("No `decoding_cfg` passed when changing decoding strategy, using internal config")
decoding_cfg = self.cfg.decoding
self.decoding = RNNTDecoding(
decoding_cfg=decoding_cfg, decoder=self.decoder, joint=self.joint, vocabulary=self.joint.vocabulary,
)
self.wer = RNNTWER(
decoding=self.decoding,
batch_dim_index=self.wer.batch_dim_index,
use_cer=self.wer.use_cer,
log_prediction=self.wer.log_prediction,
dist_sync_on_step=True,
)
# Setup fused Joint step
if self.joint.fuse_loss_wer:
self.joint.set_loss(self.loss)
self.joint.set_wer(self.wer)
# Update config
with open_dict(self.cfg.decoding):
self.cfg.decoding = decoding_cfg
logging.info(f"Changed decoding strategy to \n{OmegaConf.to_yaml(self.cfg.decoding)}")
def _setup_dataloader_from_config(self, config: Optional[Dict]):
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
# Automatically inject args from model config to dataloader config
audio_to_text_dataset.inject_dataloader_value_from_model_config(self.cfg, config, key='sample_rate')
audio_to_text_dataset.inject_dataloader_value_from_model_config(self.cfg, config, key='labels')
shuffle = config['shuffle']
device = 'gpu' if torch.cuda.is_available() else 'cpu'
if config.get('use_dali', False):
device_id = self.local_rank if device == 'gpu' else None
dataset = audio_to_text_dataset.get_dali_char_dataset(
config=config,
shuffle=shuffle,
device_id=device_id,
global_rank=self.global_rank,
world_size=self.world_size,
preprocessor_cfg=self._cfg.preprocessor,
)
return dataset
# Instantiate tarred dataset loader or normal dataset loader
if config.get('is_tarred', False):
if ('tarred_audio_filepaths' in config and config['tarred_audio_filepaths'] is None) or (
'manifest_filepath' in config and config['manifest_filepath'] is None
):
logging.warning(
"Could not load dataset as `manifest_filepath` was None or "
f"`tarred_audio_filepaths` is None. Provided config : {config}"
)
return None
shuffle_n = config.get('shuffle_n', 4 * config['batch_size']) if shuffle else 0
dataset = audio_to_text_dataset.get_tarred_char_dataset(
config=config,
shuffle_n=shuffle_n,
global_rank=self.global_rank,
world_size=self.world_size,
augmentor=augmentor,
)
shuffle = False
else:
if 'manifest_filepath' in config and config['manifest_filepath'] is None:
logging.warning(f"Could not load dataset as `manifest_filepath` was None. Provided config : {config}")
return None
dataset = audio_to_text_dataset.get_char_dataset(config=config, augmentor=augmentor)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=config['batch_size'],
collate_fn=dataset.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_training_data(self, train_data_config: Optional[Union[DictConfig, Dict]]):
"""
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.AudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.AudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text_dali.AudioToCharDALIDataset`
"""
if 'shuffle' not in train_data_config:
train_data_config['shuffle'] = True
# preserve config
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 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'])
)
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.AudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.AudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text_dali.AudioToCharDALIDataset`
"""
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)
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.AudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.AudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToCharDataset`
- :class:`~nemo.collections.asr.data.audio_to_text.TarredAudioToBPEDataset`
- :class:`~nemo.collections.asr.data.audio_to_text_dali.AudioToCharDALIDataset`
"""
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)
@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),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"outputs": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
}
@typecheck()
def forward(
self, input_signal=None, input_signal_length=None, processed_signal=None, processed_signal_length=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.
Please refer to the `training_step` in order to see the full `forward` step for training - which
performs the forward of the acoustic model, the prediction network and then the joint network.
Finally, it computes the loss and possibly compute the detokenized text via the `decoding` step.
Please refer to the `validation_step` in order to see the full `forward` step for inference - which
performs the forward of the acoustic model, the prediction network and then the joint network.
Finally, it computes the decoded tokens via the `decoding` step and possibly compute the batch metrics.
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.
Returns:
A tuple of 2 elements -
1) The log probabilities tensor of shape [B, T, D].
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)
return encoded, encoded_len
# PTL-specific methods
def training_step(self, batch, batch_nb):
signal, signal_len, transcript, transcript_len = batch
# forward() only performs encoder forward
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)
del signal
# During training, loss must be computed, so decoder forward is necessary
decoder, target_length = 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 experimental 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
)
tensorboard_logs = {'train_loss': loss_value, 'learning_rate': self._optimizer.param_groups[0]['lr']}
if (sample_id + 1) % log_every_n_steps == 0:
self.wer.update(encoded, encoded_len, transcript, transcript_len)
_, scores, words = self.wer.compute()
self.wer.reset()
tensorboard_logs.update({'training_batch_wer': scores.float() / words})
else:
# If experimental fused Joint-Loss-WER is used
if (sample_id + 1) % log_every_n_steps == 0:
compute_wer = True
else:
compute_wer = False
# 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,
)
tensorboard_logs = {'train_loss': loss_value, 'learning_rate': self._optimizer.param_groups[0]['lr']}
if compute_wer:
tensorboard_logs.update({'training_batch_wer': wer})
# Log items
self.log_dict(tensorboard_logs)
# Preserve batch acoustic model T and language model U parameters if normalizing
if self._optim_normalize_joint_txu:
self._optim_normalize_txu = [encoded_len.max(), transcript_len.max()]
return {'loss': loss_value}
def validation_step(self, batch, batch_idx, dataloader_idx=0):
signal, signal_len, transcript, transcript_len = batch
# forward() only performs encoder forward
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)
del signal
tensorboard_logs = {}
# If experimental fused Joint-Loss-WER is not used
if not self.joint.fuse_loss_wer:
if self.compute_eval_loss:
decoder, target_length = 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(encoded, encoded_len, transcript, 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 = 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
return tensorboard_logs
def test_step(self, batch, batch_idx, dataloader_idx=0):
logs = self.validation_step(batch, batch_idx, dataloader_idx=dataloader_idx)
test_logs = {
'test_wer_num': logs['val_wer_num'],
'test_wer_denom': logs['val_wer_denom'],
# 'test_wer': logs['val_wer'],
}
if 'val_loss' in logs:
test_logs['test_loss'] = logs['val_loss']
return test_logs
def multi_validation_epoch_end(self, outputs, dataloader_idx: int = 0):
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 = {}
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}
return {**val_loss_log, 'log': tensorboard_logs}
def multi_test_epoch_end(self, outputs, dataloader_idx: int = 0):
if self.compute_eval_loss:
test_loss_mean = torch.stack([x['test_loss'] for x in outputs]).mean()
test_loss_log = {'test_loss': test_loss_mean}
else:
test_loss_log = {}
wer_num = torch.stack([x['test_wer_num'] for x in outputs]).sum()
wer_denom = torch.stack([x['test_wer_denom'] for x in outputs]).sum()
tensorboard_logs = {**test_loss_log, 'test_wer': wer_num.float() / wer_denom}
return {**test_loss_log, 'log': tensorboard_logs}
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.
Returns:
A pytorch DataLoader for the given audio file(s).
"""
dl_config = {
'manifest_filepath': os.path.join(config['temp_dir'], 'manifest.json'),
'sample_rate': self.preprocessor._sample_rate,
'labels': self.joint.vocabulary,
'batch_size': min(config['batch_size'], len(config['paths2audio_files'])),
'trim_silence': True,
'shuffle': False,
}
temporary_datalayer = self._setup_dataloader_from_config(config=DictConfig(dl_config))
return temporary_datalayer
def on_after_backward(self):
super().on_after_backward()
if self._optim_variational_noise_std > 0 and self.global_step >= self._optim_variational_noise_start:
for param_name, param in self.decoder.named_parameters():
if param.grad is not None:
noise = torch.normal(
mean=0.0,
std=self._optim_variational_noise_std,
size=param.size(),
device=param.device,
dtype=param.dtype,
)
param.grad.data.add_(noise)
if self._optim_normalize_joint_txu:
T, U = self._optim_normalize_txu
if T is not None and U is not None:
for param_name, param in self.encoder.named_parameters():
if param.grad is not None:
param.grad.data.div_(U)
for param_name, param in self.decoder.named_parameters():
if param.grad is not None:
param.grad.data.div_(T)
if self._optim_normalize_encoder_norm:
for param_name, param in self.encoder.named_parameters():
if param.grad is not None:
norm = param.grad.norm()
param.grad.data.div_(norm)
if self._optim_normalize_decoder_norm:
for param_name, param in self.decoder.named_parameters():
if param.grad is not None:
norm = param.grad.norm()
param.grad.data.div_(norm)
if self._optim_normalize_joint_norm:
for param_name, param in self.joint.named_parameters():
if param.grad is not None:
norm = param.grad.norm()
param.grad.data.div_(norm)
@@ -0,0 +1,14 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.models.wav2vec.wav2vec_model import Wav2VecEncoderModel
@@ -0,0 +1,180 @@
# Copyright (c) 2020, 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.
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, Dict, List, Optional
class Wav2VecActivationType(Enum):
relu = 'relu'
gelu = 'gelu'
class Wav2VecMaskType(Enum):
"""
Used to select configuration to compute mask lengths
static = fixed size
uniform = sample from uniform distribution [mask_other, mask_length*2]
normal = sample from normal distribution with mean mask_length and stdev mask_other. mask is min 1 element
poisson = sample from possion distribution with lambda = mask length
"""
static = auto()
uniform = auto()
normal = auto()
poisson = auto()
class Wav2VecConvExtractorMode(Enum):
"""
Mode for feature extractor. default has a single group norm with d groups in the first conv block,
whereas layer_norm has layer norms in every block.
"""
default = auto()
layer_norm = auto()
@dataclass
class ConvConfig:
conv_pos: int = field(default=128, metadata={'help': 'Number of filters for convolutional positional embeddings'})
conv_pos_groups: int = field(
default=16, metadata={'help': 'Number of groups for convolutional positional embeddings'}
)
@dataclass
class Wav2VecTransformerEncoderConfig:
encoder_layers: int = field(default=12, metadata={'help': 'Number of encoder layers in transformer model'})
encoder_layerdrop: float = field(default=0.05, metadata={'help': 'Probability of dropping transformer layers'})
embedding_dim: int = field(default=768, metadata={'help': 'Encoder embedding dim'})
ffn_embedding_dim: int = field(default=3072, metadata={'help': 'Encoder embedding dim for feed forward'})
num_attention_heads: int = field(default=8, metadata={'help': 'Number of encoder attention heads'})
dropout: float = field(default=0.1, metadata={'help': 'Dropout probability for transformer encoder'})
activation_fn: Wav2VecActivationType = field(
default=Wav2VecActivationType.gelu, metadata={'help': 'Activation for transformer'}
)
layer_norm_first: bool = field(default=False, metadata={'help': 'Apply layer norm first within the transformer'})
@dataclass
class Wav2VecTransformerConfig:
dropout: float = field(default=0.1, metadata={'help': 'Dropout probability for the transformer'})
conv: ConvConfig = ConvConfig()
encoder: Wav2VecTransformerEncoderConfig = Wav2VecTransformerEncoderConfig()
@dataclass
class QuantizerConfig:
quantize_targets: bool = field(default=True, metadata={'help': 'Use quantized targets'})
quantize_input: bool = field(default=False, metadata={'help': 'Use quantized inputs'})
same_quantizer: bool = field(default=False, metadata={'help': 'Use the same quantizer for inputs and targets'})
latent_vars: int = field(
default=320, metadata={'help': 'Number of latent variables in each group of the codebook'}
)
latent_groups: int = field(default=2, metadata={'help': 'Number of groups within the codebook'})
latent_dim: int = field(
default=0,
metadata={
'help': 'If greater than 0, use dim for latent variables, else infered by final_dim / latent_groups'
},
)
latent_temp: tuple = field(
default=(2, 0.5, 0.999995), metadata={'help': 'Quantize temperature (start, stop, decay factor)'}
)
@dataclass
class ConvFeatureEncoderConfig:
extractor_mode: Wav2VecConvExtractorMode = field(default=Wav2VecConvExtractorMode.default)
conv_bias: bool = field(default=False, metadata={'help': 'Include bias in convolution feature extractor model'})
conv_feature_layers: List = field(
default_factory=lambda: [(512, 10, 5)] + [(512, 3, 2)] * 4 + [(512, 2, 2)] + [(512, 2, 2)],
metadata={'help': 'convolutional feature extraction layers [(dim, kernel_size, stride), ...'},
)
@dataclass
class LossConfig:
prob_ppl_weight: float = field(default=0.1, metadata={'help': 'Weight applied to quantized prob perplexity loss'})
feature_loss_weight: float = field(default=0, metadata={'help': 'Weight applied to feature L2 Norm'})
@dataclass
class Wav2VecMaskingConfig:
mask_prob: float = field(default=0.65, metadata={'help': 'Probability of replacing token with mask'})
mask_type: Wav2VecMaskType = field(default=Wav2VecMaskType.static,)
mask_other: int = field(
default=0,
metadata={'help': 'Secondary mask used for complex distributions (see help in compute_mask_indices)'},
)
mask_length: int = field(default=10, metadata={'help': 'Length of mask when masking time steps'})
no_mask_overlap: bool = field(default=False, metadata={'help': 'Whether to allow masks to overlap'})
mask_min_space: int = field(
default=1, metadata={'help': 'Minimum space beetween spans (if no overlap is enabled)'}
)
mask_channel_prob: float = field(default=0, metadata={'help': 'Probability of replacing a feature with 0'})
mask_channel_type: Wav2VecMaskType = field(default=Wav2VecMaskType.static,)
mask_channel_other: int = field(
default=0,
metadata={
'help': 'Secondary mask argument (used for more complex distributions (see help in compute_mask_indices)'
},
)
mask_channel_length: int = field(default=10, metadata={'help': 'Length of masks for features (channels)'})
no_mask_channel_overlap: bool = field(
default=False, metadata={'help': 'Whether to allow channel masks to overlap'}
)
mask_channel_min_space: int = field(
default=1, metadata={'help': 'Minimum space between spans (if no overlap is enabled)'}
)
@dataclass
class Wav2VecEncoderModelConfig:
loss: LossConfig = LossConfig()
quantizer: QuantizerConfig = QuantizerConfig()
conv_feature_encoder: ConvFeatureEncoderConfig = ConvFeatureEncoderConfig()
transformer_encoder: Wav2VecTransformerConfig = Wav2VecTransformerConfig()
masking: Wav2VecMaskingConfig = Wav2VecMaskingConfig()
dropout_input: float = field(default=0.1, metadata={'help': 'Dropout applied to input raw features'})
dropout_features: float = field(
default=0.1, metadata={'help': 'Dropout applied to the features generator by convolutions'}
)
final_dim: int = field(default=0, metadata={'help': 'Project final representations and targets to this dimension'})
n_negatives: int = field(
default=100, metadata={'help': 'Number of negatives to sample from the same audio sample'}
)
cross_sample_negatives: int = field(
default=0, metadata={'help': 'Number of negatives to sample from any sample in the batch'}
)
codebook_negatives: int = field(default=0, metadata={'help': 'Number of negative examples in codebook'})
negatives_from_everywhere: bool = field(
default=False, metadata={'help': 'Sample negatives from everywhere, not just masked states'}
)
logit_temp: float = field(default=0.1, metadata={'help': 'Temperature to divide logits by'})
target_glu: bool = field(default=False, metadata={'help': 'Adds project and applies GLU to targets'})
feature_grad_mult: float = field(default=0.1, metadata={'help': 'Multiply extracted feature gradients'})
train_ds: Optional[Dict[Any, Any]] = None
validation_ds: Optional[Dict[Any, Any]] = None
test_ds: Optional[Dict[Any, Any]] = None
optim: Optional[Dict[Any, Any]] = None
@@ -0,0 +1,504 @@
# Copyright (c) 2020, 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.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# Copyright (c) Facebook, Inc. and its affiliates.
#
import logging
from math import ceil
from typing import Dict, Optional, Union
import torch
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from torch import nn
from nemo.collections.asr.data import audio_to_text_dataset
from nemo.collections.asr.losses.wav2vecloss import Wav2VecLoss
from nemo.collections.asr.models.wav2vec.wav2vec_config import Wav2VecEncoderModelConfig
from nemo.collections.asr.modules.wav2vec_modules import GumbelVectorQuantizer, compute_mask_indices
from nemo.collections.asr.parts.preprocessing.perturb import process_augmentations
from nemo.collections.asr.parts.submodules.wav2vec import ConvFeatureEncoder, GradMultiply, Wav2VecTransformerEncoder
from nemo.core import ModelPT
from nemo.core.classes.common import PretrainedModelInfo, typecheck
from nemo.core.neural_types import AudioSignal, EncodedRepresentation, LossType, MaskType, NeuralType
from nemo.core.neural_types.elements import BoolType, FloatType
def buffered_arange(max):
if not hasattr(buffered_arange, "buf"):
buffered_arange.buf = torch.LongTensor()
if max > buffered_arange.buf.numel():
buffered_arange.buf.resize_(max)
torch.arange(max, out=buffered_arange.buf)
return buffered_arange.buf[:max]
class Wav2VecEncoderModel(ModelPT):
def __init__(self, cfg: DictConfig, trainer: Trainer = None):
# Get global rank and total number of GPU workers for IterableDataset partitioning, if applicable
self.global_rank = 0
self.world_size = 0
self.local_rank = 0
if trainer is not None:
self.global_rank = (trainer.node_rank * trainer.num_gpus) + trainer.local_rank
self.world_size = trainer.num_nodes * trainer.num_gpus
self.local_rank = trainer.local_rank
super().__init__(cfg=cfg, trainer=trainer)
schema = OmegaConf.structured(Wav2VecEncoderModelConfig)
if isinstance(cfg, dict):
cfg = OmegaConf.create(cfg)
elif not isinstance(cfg, DictConfig):
raise ValueError(f"cfg was type: {type(cfg)}. Expected either a dict or a DictConfig")
cfg = OmegaConf.create(OmegaConf.to_container(cfg, resolve=True))
cfg = OmegaConf.merge(schema, cfg)
feature_enc_layers = cfg.conv_feature_encoder.conv_feature_layers
self.embed = feature_enc_layers[-1][0] # Select last conv output layer dimension
self.feature_extractor = ConvFeatureEncoder(
conv_layers=feature_enc_layers,
mode=cfg.conv_feature_encoder.extractor_mode,
conv_bias=cfg.conv_feature_encoder.conv_bias,
)
encoder_embed_dim = cfg.transformer_encoder.encoder.embedding_dim
self.post_extract_proj = (
nn.Linear(self.embed, encoder_embed_dim)
if self.embed != encoder_embed_dim and not cfg.quantizer.quantize_input
else None
)
self.mask_cfg = cfg.masking
self.dropout_input = nn.Dropout(cfg.dropout_input)
self.dropout_features = nn.Dropout(cfg.dropout_features)
self.feature_grad_mult = cfg.feature_grad_mult
self.quantizer = None
self.input_quantizer = None
self.n_negatives = cfg.n_negatives
self.cross_sample_negatives = cfg.cross_sample_negatives
self.codebook_negatives = cfg.codebook_negatives
self.negatives_from_everywhere = cfg.negatives_from_everywhere
final_dim = cfg.final_dim if cfg.final_dim > 0 else encoder_embed_dim
self.final_dim = final_dim
if cfg.quantizer.quantize_targets:
vq_dim = cfg.quantizer.latent_dim if cfg.quantizer.latent_dim > 0 else final_dim
self.quantizer = GumbelVectorQuantizer(
dim=self.embed,
num_vars=cfg.quantizer.latent_vars,
temp=cfg.quantizer.latent_temp,
groups=cfg.quantizer.latent_groups,
combine_groups=False,
vq_dim=vq_dim,
time_first=True,
)
self.project_q = nn.Linear(vq_dim, final_dim)
else:
self.project_q = nn.Linear(self.embed, final_dim)
if cfg.quantizer.quantize_input:
if cfg.quantizer.same_quantizer and self.quantizer is not None:
vq_dim = final_dim
self.input_quantizer = self.quantizer
else:
vq_dim = cfg.quantizer.latent_dim if cfg.quantizer.latent_dim > 0 else encoder_embed_dim
self.input_quantizer = GumbelVectorQuantizer(
dim=self.embed,
num_vars=cfg.quantizer.latent_vars,
temp=cfg.quantizer.latent_temp,
groups=cfg.quantizer.latent_groups,
combine_groups=False,
vq_dim=vq_dim,
time_first=True,
)
self.project_inp = nn.Linear(vq_dim, encoder_embed_dim)
self.mask_emb = nn.Parameter(torch.FloatTensor(encoder_embed_dim).uniform_())
self.encoder = Wav2VecTransformerEncoder(cfg.transformer_encoder)
self.layer_norm = nn.LayerNorm(self.embed)
self.target_glu = None
if cfg.target_glu:
self.target_glu = nn.Sequential(nn.Linear(final_dim, final_dim * 2), nn.GLU())
self.final_proj = nn.Linear(encoder_embed_dim, final_dim)
self.loss = Wav2VecLoss(
feature_loss_weight=cfg.loss.feature_loss_weight,
prob_ppl_weight=cfg.loss.prob_ppl_weight,
logit_temp=cfg.logit_temp,
)
def training_step(self, batch, batch_idx):
loss, feature_loss, prob_ppl_loss = self._step(batch)
self.log('learning_rate', self._optimizer.param_groups[0]['lr'])
self.log('loss', loss, on_epoch=True)
self.log('feature_loss', feature_loss, on_epoch=True, sync_dist=True)
self.log('prob_ppl_loss', prob_ppl_loss, on_epoch=True, sync_dist=True)
return {'loss': loss}
def validation_step(self, batch, batch_idx, dataloader_idx=0):
loss, feature_loss, prob_ppl_loss = self._step(batch)
self.log('val_loss', loss, prog_bar=True, on_epoch=True, sync_dist=True)
def test_step(self, batch, batch_idx, dataloader_idx=0):
loss, feature_loss, prob_ppl_loss = self._step(batch)
self.log('test_loss', loss, prog_bar=True, on_epoch=True, sync_dist=True)
def _step(self, batch):
audio_signal, audio_lengths, _, _ = batch
padding_mask = self._create_padding_mask(audio_lengths)
self._update_quantizer_temp()
logits, targets, sampled_negatives, _, features_penalty, prob_ppl_loss, _ = self(
source=audio_signal, padding_mask=padding_mask
)
loss, feature_loss, prob_ppl_loss = self.loss(
logits=logits,
targets=targets,
negatives=sampled_negatives,
prob_ppl_loss=prob_ppl_loss,
feature_loss=features_penalty,
)
return loss, feature_loss, prob_ppl_loss
@classmethod
def list_available_models(cls) -> Optional[PretrainedModelInfo]:
return None
@property
def input_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"source": NeuralType(('B', 'T'), AudioSignal()),
"padding_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"mask": NeuralType(elements_type=BoolType(), optional=True),
"features_only": NeuralType(elements_type=BoolType(), optional=True),
}
@property
def output_types(self) -> Optional[Dict[str, NeuralType]]:
return {
"logits": NeuralType(('B', 'T', 'D'), EncodedRepresentation()),
"targets": NeuralType(('B', 'T', 'D'), EncodedRepresentation(), optional=True),
"sampled_negatives": NeuralType(('N', 'B', 'T', 'D'), EncodedRepresentation(), optional=True),
"padding_mask": NeuralType(('B', 'T'), MaskType(), optional=True),
"features_penalty": NeuralType(elements_type=LossType(), optional=True),
"prob_ppl_loss": NeuralType(elements_type=LossType(), optional=True),
"cur_codebook_temp": NeuralType(elements_type=FloatType(), optional=True),
}
@typecheck()
def forward(self, source, padding_mask=None, mask=True, features_only=False) -> tuple:
prob_ppl_loss, cur_codebook_temp = None, None
if self.feature_grad_mult > 0:
features = self.feature_extractor(source)
if self.feature_grad_mult != 1.0:
features = GradMultiply.apply(features, self.feature_grad_mult)
else:
with torch.no_grad():
features = self.feature_extractor(source)
features_penalty = features.float().pow(2).mean() # L2 Norm on features
features = features.transpose(1, 2)
features = self.layer_norm(features)
unmasked_features = features.clone()
if padding_mask is not None:
extra = padding_mask.size(1) % features.size(1)
if extra > 0:
padding_mask = padding_mask[:, :-extra]
padding_mask = padding_mask.view(padding_mask.size(0), features.size(1), -1)
padding_mask = padding_mask.all(-1)
if self.post_extract_proj is not None:
features = self.post_extract_proj(features)
features = self.dropout_input(features)
unmasked_features = self.dropout_features(unmasked_features)
if self.input_quantizer:
features, prob_ppl_loss, cur_codebook_temp = self.input_quantizer(features)
features = self.project_inp(features)
if mask:
logits, mask_indices = self.apply_mask(features, padding_mask)
if mask_indices is not None:
targets = unmasked_features[mask_indices].view(
unmasked_features.size(0), -1, unmasked_features.size(-1)
)
else:
targets = unmasked_features
else:
logits = features
targets = unmasked_features
mask_indices = None
logits = self.encoder(logits, padding_mask=padding_mask)
if features_only:
return logits, padding_mask
if self.quantizer:
targets, prob_ppl_loss, cur_codebook_temp = self.quantizer(targets)
targets = self.project_q(targets)
if self.negatives_from_everywhere:
neg_cands, *_ = self.quantizer(unmasked_features)
sampled_negatives, _ = self.sample_negatives(neg_cands, targets.size(1))
sampled_negatives = self.project_q(sampled_negatives)
else:
sampled_negatives, _ = self.sample_negatives(targets, targets.size(1))
if self.codebook_negatives > 0:
cb_negs = self.quantizer.sample_from_codebook(
targets.size(0) * targets.size(1), self.codebook_negatives
)
cb_negs = cb_negs.view(
self.codebook_negatives, targets.size(0), targets.size(1), -1
) # order doesnt matter
cb_negs = self.project_q(cb_negs)
sampled_negatives = torch.cat([sampled_negatives, cb_negs], dim=0)
else:
targets = self.project_q(targets)
if self.negatives_from_everywhere:
sampled_negatives, _ = self.sample_negatives(unmasked_features, targets.size(1))
sampled_negatives = self.project_q(sampled_negatives)
else:
sampled_negatives, _ = self.sample_negatives(targets, targets.size(1))
logits = logits[mask_indices].view(logits.size(0), -1, logits.size(-1))
if self.target_glu:
targets = self.target_glu(targets)
sampled_negatives = self.target_glu(sampled_negatives)
logits = self.final_proj(logits)
return logits, targets, sampled_negatives, padding_mask, features_penalty, prob_ppl_loss, cur_codebook_temp
def extract_features(self, source, audio_lengths, mask=False):
padding_mask = self._create_padding_mask(audio_lengths)
return self(source=source, padding_mask=padding_mask, mask=mask, features_only=True)
def remove_pretraining_modules(self):
self.quantizer = None
self.project_q = None
self.target_glu = None
self.final_proj = None
def _update_quantizer_temp(self):
if self.quantizer:
self.quantizer.set_num_updates(self.trainer.global_step)
if self.input_quantizer:
self.input_quantizer.set_num_updates(self.trainer.global_step)
def apply_mask(self, x, padding_mask):
B, T, C = x.shape
if self.mask_cfg.mask_prob > 0:
mask_indices = compute_mask_indices(
(B, T),
padding_mask,
self.mask_cfg.mask_prob,
self.mask_cfg.mask_length,
self.mask_cfg.mask_type,
self.mask_cfg.mask_other,
min_masks=2,
no_overlap=self.mask_cfg.no_mask_overlap,
min_space=self.mask_cfg.mask_min_space,
)
mask_indices = torch.from_numpy(mask_indices).to(x.device)
mask_emb = self.mask_emb.type_as(x)
x[mask_indices] = mask_emb
else:
mask_indices = None
if self.mask_cfg.mask_channel_prob > 0:
mask_channel_indices = compute_mask_indices(
(B, C),
None,
self.mask_cfg.mask_channel_prob,
self.mask_cfg.mask_channel_length,
self.mask_cfg.mask_channel_type,
self.mask_cfg.mask_channel_other,
no_overlap=self.mask_cfg.no_mask_channel_overlap,
min_space=self.mask_cfg.mask_channel_min_space,
)
mask_channel_indices = torch.from_numpy(mask_channel_indices).to(x.device).unsqueeze(1).expand(-1, T, -1)
x[mask_channel_indices] = 0
return x, mask_indices
def sample_negatives(self, y, num):
if self.n_negatives == 0 and self.cross_sample_negatives == 0:
return y.new(0)
bsz, tsz, fsz = y.shape
y = y.view(-1, fsz) # BTC => (BxT)C
cross_high = tsz * bsz
high = tsz
with torch.no_grad():
assert high > 1, f"{bsz, tsz, fsz}"
if self.n_negatives > 0:
tszs = buffered_arange(num).unsqueeze(-1).expand(-1, self.n_negatives).flatten()
neg_idxs = torch.randint(low=0, high=high - 1, size=(bsz, self.n_negatives * num))
neg_idxs[neg_idxs >= tszs] += 1
if self.cross_sample_negatives > 0:
tszs = buffered_arange(num).unsqueeze(-1).expand(-1, self.cross_sample_negatives).flatten()
cross_neg_idxs = torch.randint(
low=0, high=cross_high - 1, size=(bsz, self.cross_sample_negatives * num),
)
cross_neg_idxs[cross_neg_idxs >= tszs] += 1
if self.n_negatives > 0:
for i in range(1, bsz):
neg_idxs[i] += i * high
else:
neg_idxs = cross_neg_idxs
if self.cross_sample_negatives > 0 and self.n_negatives > 0:
neg_idxs = torch.cat([neg_idxs, cross_neg_idxs], dim=1)
negs = y[neg_idxs.view(-1)]
negs = negs.view(bsz, num, self.n_negatives + self.cross_sample_negatives, fsz).permute(
2, 0, 1, 3
) # to NxBxTxC
return negs, neg_idxs
def _create_padding_mask(self, audio_lengths):
# Broadcast to vectorize creating the padding mask
max_len = max(audio_lengths)
padding_mask = torch.arange(max_len, device=self.device)
padding_mask = padding_mask.expand(len(audio_lengths), max_len) < audio_lengths.unsqueeze(1)
# Negate to false where no padding
padding_mask = ~padding_mask
return padding_mask
def setup_training_data(self, train_data_config: Optional[Union[DictConfig, Dict]]):
if 'shuffle' not in train_data_config:
train_data_config['shuffle'] = True
# preserve config
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 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'])
)
def setup_validation_data(self, val_data_config: Optional[Union[DictConfig, Dict]]):
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)
def setup_test_data(self, test_data_config: Optional[Union[DictConfig, Dict]]):
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)
def _setup_dataloader_from_config(self, config: Optional[Dict]):
if 'augmentor' in config:
augmentor = process_augmentations(config['augmentor'])
else:
augmentor = None
shuffle = config['shuffle']
device = 'gpu' if torch.cuda.is_available() else 'cpu'
if config.get('use_dali', False):
device_id = self.local_rank if device == 'gpu' else None
dataset = audio_to_text_dataset.get_dali_char_dataset(
config=config,
shuffle=shuffle,
device_id=device_id,
global_rank=self.global_rank,
world_size=self.world_size,
preprocessor_cfg=self._cfg.preprocessor,
)
return dataset
# Instantiate tarred dataset loader or normal dataset loader
if config.get('is_tarred', False):
if ('tarred_audio_filepaths' in config and config['tarred_audio_filepaths'] is None) or (
'manifest_filepath' in config and config['manifest_filepath'] is None
):
logging.warning(
"Could not load dataset as `manifest_filepath` was None or "
f"`tarred_audio_filepaths` is None. Provided config : {config}"
)
return None
shuffle_n = config.get('shuffle_n', 4 * config['batch_size']) if shuffle else 0
dataset = audio_to_text_dataset.get_tarred_char_dataset(
config=config,
shuffle_n=shuffle_n,
global_rank=self.global_rank,
world_size=self.world_size,
augmentor=augmentor,
)
shuffle = False
else:
if 'manifest_filepath' in config and config['manifest_filepath'] is None:
logging.warning(f"Could not load dataset as `manifest_filepath` was None. Provided config : {config}")
return None
dataset = audio_to_text_dataset.get_char_dataset(config=config, augmentor=augmentor)
return torch.utils.data.DataLoader(
dataset=dataset,
batch_size=config['batch_size'],
collate_fn=dataset.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),
)
@@ -0,0 +1,30 @@
# Copyright (c) 2020, 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.
from nemo.collections.asr.modules.audio_preprocessing import (
AudioToMelSpectrogramPreprocessor,
AudioToMFCCPreprocessor,
CropOrPadSpectrogramAugmentation,
SpectrogramAugmentation,
)
from nemo.collections.asr.modules.beam_search_decoder import BeamSearchDecoderWithLM
from nemo.collections.asr.modules.conformer_encoder import ConformerEncoder
from nemo.collections.asr.modules.conv_asr import (
ConvASRDecoder,
ConvASRDecoderClassification,
ConvASREncoder,
SpeakerDecoder,
)
from nemo.collections.asr.modules.lstm_decoder import LSTMDecoder
from nemo.collections.asr.modules.rnnt import RNNTDecoder, RNNTJoint
@@ -0,0 +1,643 @@
# Copyright (c) 2020, 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 math
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Optional
import torch
from packaging import version
from nemo.collections.asr.parts.numba.spec_augment import SpecAugmentNumba, spec_augment_launch_heuristics
from nemo.collections.asr.parts.preprocessing.features import FilterbankFeatures
from nemo.collections.asr.parts.submodules.spectr_augment import SpecAugment, SpecCutout
from nemo.core.classes import NeuralModule, typecheck
from nemo.core.neural_types import (
AudioSignal,
LengthsType,
MelSpectrogramType,
MFCCSpectrogramType,
NeuralType,
SpectrogramType,
)
from nemo.core.utils import numba_utils
from nemo.core.utils.numba_utils import __NUMBA_MINIMUM_VERSION__
from nemo.utils import logging
try:
import torchaudio
import torchaudio.functional
import torchaudio.transforms
TORCHAUDIO_VERSION = version.parse(torchaudio.__version__)
TORCHAUDIO_VERSION_MIN = version.parse('0.5')
HAVE_TORCHAUDIO = True
except ModuleNotFoundError:
HAVE_TORCHAUDIO = False
__all__ = [
'AudioToMelSpectrogramPreprocessor',
'AudioToMFCCPreprocessor',
'SpectrogramAugmentation',
'CropOrPadSpectrogramAugmentation',
]
class AudioPreprocessor(NeuralModule, ABC):
"""
An interface for Neural Modules that performs audio pre-processing,
transforming the wav files to features.
"""
def __init__(self, win_length, hop_length):
super().__init__()
self.win_length = win_length
self.hop_length = hop_length
self.torch_windows = {
'hann': torch.hann_window,
'hamming': torch.hamming_window,
'blackman': torch.blackman_window,
'bartlett': torch.bartlett_window,
'ones': torch.ones,
None: torch.ones,
}
@typecheck()
@torch.no_grad()
def forward(self, input_signal, length):
processed_signal, processed_length = self.get_features(input_signal, length)
return processed_signal, processed_length
@abstractmethod
def get_features(self, input_signal, length):
# Called by forward(). Subclasses should implement this.
pass
class AudioToMelSpectrogramPreprocessor(AudioPreprocessor):
"""Featurizer module that converts wavs to mel spectrograms.
We don't use torchaudio's implementation here because the original
implementation is not the same, so for the sake of backwards-compatibility
this will use the old FilterbankFeatures for now.
Args:
sample_rate (int): Sample rate of the input audio data.
Defaults to 16000
window_size (float): Size of window for fft in seconds
Defaults to 0.02
window_stride (float): Stride of window for fft in seconds
Defaults to 0.01
n_window_size (int): Size of window for fft in samples
Defaults to None. Use one of window_size or n_window_size.
n_window_stride (int): Stride of window for fft in samples
Defaults to None. Use one of window_stride or n_window_stride.
window (str): Windowing function for fft. can be one of ['hann',
'hamming', 'blackman', 'bartlett']
Defaults to "hann"
normalize (str): Can be one of ['per_feature', 'all_features']; all
other options disable feature normalization. 'all_features'
normalizes the entire spectrogram to be mean 0 with std 1.
'pre_features' normalizes per channel / freq instead.
Defaults to "per_feature"
n_fft (int): Length of FT window. If None, it uses the smallest power
of 2 that is larger than n_window_size.
Defaults to None
preemph (float): Amount of pre emphasis to add to audio. Can be
disabled by passing None.
Defaults to 0.97
features (int): Number of mel spectrogram freq bins to output.
Defaults to 64
lowfreq (int): Lower bound on mel basis in Hz.
Defaults to 0
highfreq (int): Lower bound on mel basis in Hz.
Defaults to None
log (bool): Log features.
Defaults to True
log_zero_guard_type(str): Need to avoid taking the log of zero. There
are two options: "add" or "clamp".
Defaults to "add".
log_zero_guard_value(float, or str): Add or clamp requires the number
to add with or clamp to. log_zero_guard_value can either be a float
or "tiny" or "eps". torch.finfo is used if "tiny" or "eps" is
passed.
Defaults to 2**-24.
dither (float): Amount of white-noise dithering.
Defaults to 1e-5
pad_to (int): Ensures that the output size of the time dimension is
a multiple of pad_to.
Defaults to 16
frame_splicing (int): Defaults to 1
exact_pad (bool): If True, sets stft center to False and adds padding, such that num_frames = audio_length
// hop_length. Defaults to False.
stft_exact_pad (bool): If True, uses pytorch_stft and convolutions with
padding such that num_frames = num_samples / hop_length. If False,
stft_conv will be used to determine how stft will be performed.
Defaults to False. TODO:This feature is deprecated and will be removed in 1.1.0
stft_conv (bool): If True, uses pytorch_stft and convolutions. If
False, uses torch.stft. TODO:This feature is deprecated and will be removed in 1.1.0
Defaults to False
pad_value (float): The value that shorter mels are padded with.
Defaults to 0
mag_power (float): The power that the linear spectrogram is raised to
prior to multiplication with mel basis.
Defaults to 2 for a power spec
"""
def save_to(self, save_path: str):
pass
@classmethod
def restore_from(cls, restore_path: str):
pass
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"input_signal": NeuralType(('B', 'T'), AudioSignal(freq=self._sample_rate)),
"length": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
processed_signal:
0: AxisType(BatchTag)
1: AxisType(MelSpectrogramSignalTag)
2: AxisType(ProcessedTimeTag)
processed_length:
0: AxisType(BatchTag)
"""
return {
"processed_signal": NeuralType(('B', 'D', 'T'), MelSpectrogramType()),
"processed_length": NeuralType(tuple('B'), LengthsType()),
}
def __init__(
self,
sample_rate=16000,
window_size=0.02,
window_stride=0.01,
n_window_size=None,
n_window_stride=None,
window="hann",
normalize="per_feature",
n_fft=None,
preemph=0.97,
features=64,
lowfreq=0,
highfreq=None,
log=True,
log_zero_guard_type="add",
log_zero_guard_value=2 ** -24,
dither=1e-5,
pad_to=16,
frame_splicing=1,
exact_pad=False,
stft_exact_pad=False,
stft_conv=False,
pad_value=0,
mag_power=2.0,
):
super().__init__(n_window_size, n_window_stride)
self._sample_rate = sample_rate
if window_size and n_window_size:
raise ValueError(f"{self} received both window_size and " f"n_window_size. Only one should be specified.")
if window_stride and n_window_stride:
raise ValueError(
f"{self} received both window_stride and " f"n_window_stride. Only one should be specified."
)
if window_size:
n_window_size = int(window_size * self._sample_rate)
if window_stride:
n_window_stride = int(window_stride * self._sample_rate)
self.featurizer = FilterbankFeatures(
sample_rate=self._sample_rate,
n_window_size=n_window_size,
n_window_stride=n_window_stride,
window=window,
normalize=normalize,
n_fft=n_fft,
preemph=preemph,
nfilt=features,
lowfreq=lowfreq,
highfreq=highfreq,
log=log,
log_zero_guard_type=log_zero_guard_type,
log_zero_guard_value=log_zero_guard_value,
dither=dither,
pad_to=pad_to,
frame_splicing=frame_splicing,
exact_pad=exact_pad,
stft_exact_pad=stft_exact_pad,
stft_conv=stft_conv,
pad_value=pad_value,
mag_power=mag_power,
)
def get_features(self, input_signal, length):
return self.featurizer(input_signal, length)
@property
def filter_banks(self):
return self.featurizer.filter_banks
class AudioToMFCCPreprocessor(AudioPreprocessor):
"""Preprocessor that converts wavs to MFCCs.
Uses torchaudio.transforms.MFCC.
Args:
sample_rate: The sample rate of the audio.
Defaults to 16000.
window_size: Size of window for fft in seconds. Used to calculate the
win_length arg for mel spectrogram.
Defaults to 0.02
window_stride: Stride of window for fft in seconds. Used to caculate
the hop_length arg for mel spect.
Defaults to 0.01
n_window_size: Size of window for fft in samples
Defaults to None. Use one of window_size or n_window_size.
n_window_stride: Stride of window for fft in samples
Defaults to None. Use one of window_stride or n_window_stride.
window: Windowing function for fft. can be one of ['hann',
'hamming', 'blackman', 'bartlett', 'none', 'null'].
Defaults to 'hann'
n_fft: Length of FT window. If None, it uses the smallest power of 2
that is larger than n_window_size.
Defaults to None
lowfreq (int): Lower bound on mel basis in Hz.
Defaults to 0
highfreq (int): Lower bound on mel basis in Hz.
Defaults to None
n_mels: Number of mel filterbanks.
Defaults to 64
n_mfcc: Number of coefficients to retain
Defaults to 64
dct_type: Type of discrete cosine transform to use
norm: Type of norm to use
log: Whether to use log-mel spectrograms instead of db-scaled.
Defaults to True.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"input_signal": NeuralType(('B', 'T'), AudioSignal(freq=self._sample_rate)),
"length": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {
"processed_signal": NeuralType(('B', 'D', 'T'), MFCCSpectrogramType()),
"processed_length": NeuralType(tuple('B'), LengthsType()),
}
def save_to(self, save_path: str):
pass
@classmethod
def restore_from(cls, restore_path: str):
pass
def __init__(
self,
sample_rate=16000,
window_size=0.02,
window_stride=0.01,
n_window_size=None,
n_window_stride=None,
window='hann',
n_fft=None,
lowfreq=0.0,
highfreq=None,
n_mels=64,
n_mfcc=64,
dct_type=2,
norm='ortho',
log=True,
):
self._sample_rate = sample_rate
if not HAVE_TORCHAUDIO:
logging.error('Could not import torchaudio. Some features might not work.')
raise ModuleNotFoundError(
"torchaudio is not installed but is necessary for "
"AudioToMFCCPreprocessor. We recommend you try "
"building it from source for the PyTorch version you have."
)
if window_size and n_window_size:
raise ValueError(f"{self} received both window_size and " f"n_window_size. Only one should be specified.")
if window_stride and n_window_stride:
raise ValueError(
f"{self} received both window_stride and " f"n_window_stride. Only one should be specified."
)
# Get win_length (n_window_size) and hop_length (n_window_stride)
if window_size:
n_window_size = int(window_size * self._sample_rate)
if window_stride:
n_window_stride = int(window_stride * self._sample_rate)
super().__init__(n_window_size, n_window_stride)
mel_kwargs = {}
mel_kwargs['f_min'] = lowfreq
mel_kwargs['f_max'] = highfreq
mel_kwargs['n_mels'] = n_mels
mel_kwargs['n_fft'] = n_fft or 2 ** math.ceil(math.log2(n_window_size))
mel_kwargs['win_length'] = n_window_size
mel_kwargs['hop_length'] = n_window_stride
# Set window_fn. None defaults to torch.ones.
window_fn = self.torch_windows.get(window, None)
if window_fn is None:
raise ValueError(
f"Window argument for AudioProcessor is invalid: {window}."
f"For no window function, use 'ones' or None."
)
mel_kwargs['window_fn'] = window_fn
# Use torchaudio's implementation of MFCCs as featurizer
self.featurizer = torchaudio.transforms.MFCC(
sample_rate=self._sample_rate,
n_mfcc=n_mfcc,
dct_type=dct_type,
norm=norm,
log_mels=log,
melkwargs=mel_kwargs,
)
def get_features(self, input_signal, length):
features = self.featurizer(input_signal)
seq_len = torch.ceil(length.to(torch.float32) / self.hop_length).to(dtype=torch.long)
return features, seq_len
class SpectrogramAugmentation(NeuralModule):
"""
Performs time and freq cuts in one of two ways.
SpecAugment zeroes out vertical and horizontal sections as described in
SpecAugment (https://arxiv.org/abs/1904.08779). Arguments for use with
SpecAugment are `freq_masks`, `time_masks`, `freq_width`, and `time_width`.
SpecCutout zeroes out rectangulars as described in Cutout
(https://arxiv.org/abs/1708.04552). Arguments for use with Cutout are
`rect_masks`, `rect_freq`, and `rect_time`.
Args:
freq_masks (int): how many frequency segments should be cut.
Defaults to 0.
time_masks (int): how many time segments should be cut
Defaults to 0.
freq_width (int): maximum number of frequencies to be cut in one
segment.
Defaults to 10.
time_width (int): maximum number of time steps to be cut in one
segment
Defaults to 10.
rect_masks (int): how many rectangular masks should be cut
Defaults to 0.
rect_freq (int): maximum size of cut rectangles along the frequency
dimension
Defaults to 5.
rect_time (int): maximum size of cut rectangles along the time
dimension
Defaults to 25.
"""
@property
def input_types(self):
"""Returns definitions of module input types
"""
return {
"input_spec": NeuralType(('B', 'D', 'T'), SpectrogramType()),
"length": NeuralType(tuple('B'), LengthsType(), optional=True),
}
@property
def output_types(self):
"""Returns definitions of module output types
"""
return {"augmented_spec": NeuralType(('B', 'D', 'T'), SpectrogramType())}
def __init__(
self,
freq_masks=0,
time_masks=0,
freq_width=10,
time_width=10,
rect_masks=0,
rect_time=5,
rect_freq=20,
rng=None,
mask_value=0.0,
use_numba_spec_augment: bool = True,
):
super().__init__()
if rect_masks > 0:
self.spec_cutout = SpecCutout(rect_masks=rect_masks, rect_time=rect_time, rect_freq=rect_freq, rng=rng,)
# self.spec_cutout.to(self._device)
else:
self.spec_cutout = lambda input_spec: input_spec
if freq_masks + time_masks > 0:
self.spec_augment = SpecAugment(
freq_masks=freq_masks,
time_masks=time_masks,
freq_width=freq_width,
time_width=time_width,
rng=rng,
mask_value=mask_value,
)
else:
self.spec_augment = lambda input_spec: input_spec
# Check if numba is supported, and use a Numba kernel if it is
if use_numba_spec_augment and numba_utils.numba_cuda_is_supported(__NUMBA_MINIMUM_VERSION__):
self.spec_augment_numba = SpecAugmentNumba(
freq_masks=freq_masks,
time_masks=time_masks,
freq_width=freq_width,
time_width=time_width,
rng=rng,
mask_value=mask_value,
)
else:
self.spec_augment_numba = None
@typecheck()
def forward(self, input_spec, length=None):
augmented_spec = self.spec_cutout(input_spec=input_spec)
# To run the Numba kernel, correct numba version is required as well as
# tensor must be on GPU and length must be provided
if self.spec_augment_numba is not None and spec_augment_launch_heuristics(augmented_spec, length):
augmented_spec = self.spec_augment_numba(input_spec=augmented_spec, length=length)
else:
augmented_spec = self.spec_augment(input_spec=augmented_spec)
return augmented_spec
class CropOrPadSpectrogramAugmentation(NeuralModule):
"""
Pad or Crop the incoming Spectrogram to a certain shape.
Args:
audio_length (int): the final number of timesteps that is required.
The signal will be either padded or cropped temporally to this
size.
"""
def __init__(self, audio_length):
super(CropOrPadSpectrogramAugmentation, self).__init__()
self.audio_length = audio_length
@typecheck()
@torch.no_grad()
def forward(self, input_signal, length):
image = input_signal
num_images = image.shape[0]
audio_length = self.audio_length
image_len = image.shape[-1]
# Crop long signal
if image_len > audio_length: # randomly slice
cutout_images = []
offset = torch.randint(low=0, high=image_len - audio_length + 1, size=[num_images])
for idx, offset in enumerate(offset):
cutout_images.append(image[idx : idx + 1, :, offset : offset + audio_length])
image = torch.cat(cutout_images, dim=0)
del cutout_images
else: # symmetrically pad short signal with zeros
pad_left = (audio_length - image_len) // 2
pad_right = (audio_length - image_len) // 2
if (audio_length - image_len) % 2 == 1:
pad_right += 1
image = torch.nn.functional.pad(image, [pad_left, pad_right], mode="constant", value=0)
# Replace dynamic length sequences with static number of timesteps
length = (length * 0) + audio_length
return image, length
@property
def input_types(self):
"""Returns definitions of module output ports.
"""
return {
"input_signal": NeuralType(('B', 'D', 'T'), SpectrogramType()),
"length": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {
"processed_signal": NeuralType(('B', 'D', 'T'), SpectrogramType()),
"processed_length": NeuralType(tuple('B'), LengthsType()),
}
def save_to(self, save_path: str):
pass
@classmethod
def restore_from(cls, restore_path: str):
pass
@dataclass
class AudioToMelSpectrogramPreprocessorConfig:
_target_: str = "nemo.collections.asr.modules.AudioToMelSpectrogramPreprocessor"
sample_rate: int = 16000
window_size: float = 0.02
window_stride: float = 0.01
n_window_size: Optional[int] = None
n_window_stride: Optional[int] = None
window: str = "hann"
normalize: str = "per_feature"
n_fft: Optional[int] = None
preemph: float = 0.97
features: int = 64
lowfreq: int = 0
highfreq: Optional[int] = None
log: bool = True
log_zero_guard_type: str = "add"
log_zero_guard_value: float = 2 ** -24
dither: float = 1e-5
pad_to: int = 16
frame_splicing: int = 1
exact_pad: bool = False
stft_exact_pad: bool = False
stft_conv: bool = False
pad_value: int = 0
mag_power: float = 2.0
@dataclass
class AudioToMFCCPreprocessorConfig:
_target_: str = 'nemo.collections.asr.modules.AudioToMFCCPreprocessor'
sample_rate: int = 16000
window_size: float = 0.02
window_stride: float = 0.01
n_window_size: Optional[int] = None
n_window_stride: Optional[int] = None
window: str = 'hann'
n_fft: Optional[int] = None
lowfreq: Optional[float] = 0.0
highfreq: Optional[float] = None
n_mels: int = 64
n_mfcc: int = 64
dct_type: int = 2
norm: str = 'ortho'
log: bool = True
@dataclass
class SpectrogramAugmentationConfig:
_target_: str = "nemo.collections.asr.modules.SpectrogramAugmentation"
freq_masks: int = 0
time_masks: int = 0
freq_width: int = 0
time_width: Optional[Any] = 0
rect_masks: int = 0
rect_time: int = 0
rect_freq: int = 0
mask_value: float = 0
rng: Optional[Any] = None # random.Random() type
use_numba_spec_augment: bool = True
@dataclass
class CropOrPadSpectrogramAugmentationConfig:
audio_length: int
_target_: str = "nemo.collections.asr.modules.CropOrPadSpectrogramAugmentation"
@@ -0,0 +1,103 @@
# Copyright (c) 2020, 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 torch
from nemo.core.classes import NeuralModule, typecheck
from nemo.core.neural_types import LengthsType, LogprobsType, NeuralType, PredictionsType
class BeamSearchDecoderWithLM(NeuralModule):
"""Neural Module that does CTC beam search with a N-gram language model.
It takes a batch of log_probabilities. Note the bigger the batch, the
better as processing is parallelized. Outputs a list of size batch_size.
Each element in the list is a list of size beam_search, and each element
in that list is a tuple of (final_log_prob, hyp_string).
Args:
vocab (list): List of characters that can be output by the ASR model. For English, this is the 28 character set
{a-z '}. The CTC blank symbol is automatically added.
beam_width (int): Size of beams to keep and expand upon. Larger beams result in more accurate but slower
predictions
alpha (float): The amount of importance to place on the N-gram language model. Larger alpha means more
importance on the LM and less importance on the acoustic model.
beta (float): A penalty term given to longer word sequences. Larger beta will result in shorter sequences.
lm_path (str): Path to N-gram language model
num_cpus (int): Number of CPUs to use
cutoff_prob (float): Cutoff probability in vocabulary pruning, default 1.0, no pruning
cutoff_top_n (int): Cutoff number in pruning, only top cutoff_top_n characters with highest probs in
vocabulary will be used in beam search, default 40.
input_tensor (bool): Set to True if you intend to pass PyTorch Tensors, set to False if you intend to pass
NumPy arrays.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"log_probs": NeuralType(('B', 'T', 'D'), LogprobsType()),
"log_probs_length": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"predictions": NeuralType(('B', 'T'), PredictionsType())}
def __init__(
self, vocab, beam_width, alpha, beta, lm_path, num_cpus, cutoff_prob=1.0, cutoff_top_n=40, input_tensor=False
):
try:
from ctc_decoders import Scorer, ctc_beam_search_decoder_batch
except ModuleNotFoundError:
raise ModuleNotFoundError(
"BeamSearchDecoderWithLM requires the installation of ctc_decoders "
"from scripts/asr_language_modeling/ngram_lm/install_beamsearch_decoders.sh"
)
super().__init__()
if lm_path is not None:
self.scorer = Scorer(alpha, beta, model_path=lm_path, vocabulary=vocab)
else:
self.scorer = None
self.beam_search_func = ctc_beam_search_decoder_batch
self.vocab = vocab
self.beam_width = beam_width
self.num_cpus = num_cpus
self.cutoff_prob = cutoff_prob
self.cutoff_top_n = cutoff_top_n
self.input_tensor = input_tensor
@typecheck(ignore_collections=True)
@torch.no_grad()
def forward(self, log_probs, log_probs_length):
probs_list = log_probs
if self.input_tensor:
probs = torch.exp(log_probs)
probs_list = []
for i, prob in enumerate(probs):
probs_list.append(prob[: log_probs_length[i], :])
res = self.beam_search_func(
probs_list,
self.vocab,
beam_size=self.beam_width,
num_processes=self.num_cpus,
ext_scoring_func=self.scorer,
cutoff_prob=self.cutoff_prob,
cutoff_top_n=self.cutoff_top_n,
)
return res
@@ -0,0 +1,255 @@
# Copyright (c) 2020, 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 math
from collections import OrderedDict
import torch
import torch.nn as nn
from nemo.collections.asr.parts.submodules.conformer_modules import ConformerLayer
from nemo.collections.asr.parts.submodules.multi_head_attention import PositionalEncoding, RelPositionalEncoding
from nemo.collections.asr.parts.submodules.subsampling import ConvSubsampling
from nemo.core.classes.common import typecheck
from nemo.core.classes.exportable import Exportable
from nemo.core.classes.module import NeuralModule
from nemo.core.neural_types import AcousticEncodedRepresentation, LengthsType, NeuralType, SpectrogramType
__all__ = ['ConformerEncoder']
class ConformerEncoder(NeuralModule, Exportable):
"""
The encoder for ASR model of Conformer.
Based on this paper:
'Conformer: Convolution-augmented Transformer for Speech Recognition' by Anmol Gulati et al.
https://arxiv.org/abs/2005.08100
Args:
feat_in (int): the size of feature channels
n_layers (int): number of layers of ConformerBlock
d_model (int): the hidden size of the model
feat_out (int): the size of the output features
Defaults to -1 (means feat_out is d_model)
subsampling (str): the method of subsampling, choices=['vggnet', 'striding']
Defaults to striding.
subsampling_factor (int): the subsampling factor which should be power of 2
Defaults to 4.
subsampling_conv_channels (int): the size of the convolutions in the subsampling module
Defaults to -1 which would set it to d_model.
ff_expansion_factor (int): the expansion factor in feed forward layers
Defaults to 4.
self_attention_model (str): type of the attention layer and positional encoding
'rel_pos': relative positional embedding and Transformer-XL
'abs_pos': absolute positional embedding and Transformer
default is rel_pos.
pos_emb_max_len (int): the maximum length of positional embeddings
Defaulst to 5000
n_heads (int): number of heads in multi-headed attention layers
Defaults to 4.
xscaling (bool): enables scaling the inputs to the multi-headed attention layers by sqrt(d_model)
Defaults to True.
untie_biases (bool): whether to not share (untie) the bias weights between layers of Transformer-XL
Defaults to True.
conv_kernel_size (int): the size of the convolutions in the convolutional modules
Defaults to 31.
dropout (float): the dropout rate used in all layers except the attention layers
Defaults to 0.1.
dropout_emb (float): the dropout rate used for the positional embeddings
Defaults to 0.1.
dropout_att (float): the dropout rate used for the attention layer
Defaults to 0.0.
"""
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
input_example = torch.randn(16, self._feat_in, 256).to(next(self.parameters()).device)
input_example_length = torch.randint(0, 256, (16,)).to(next(self.parameters()).device)
return tuple([input_example, input_example_length])
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return OrderedDict(
{
"audio_signal": NeuralType(('B', 'D', 'T'), SpectrogramType()),
"length": NeuralType(tuple('B'), LengthsType()),
}
)
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return OrderedDict(
{
"outputs": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
}
)
def __init__(
self,
feat_in,
n_layers,
d_model,
feat_out=-1,
subsampling='striding',
subsampling_factor=4,
subsampling_conv_channels=-1,
ff_expansion_factor=4,
self_attention_model='rel_pos',
n_heads=4,
att_context_size=None,
xscaling=True,
untie_biases=True,
pos_emb_max_len=5000,
conv_kernel_size=31,
dropout=0.1,
dropout_emb=0.1,
dropout_att=0.0,
):
super().__init__()
d_ff = d_model * ff_expansion_factor
self.d_model = d_model
self._feat_in = feat_in
self.scale = math.sqrt(self.d_model)
if att_context_size:
self.att_context_size = att_context_size
else:
self.att_context_size = [-1, -1]
if xscaling:
self.xscale = math.sqrt(d_model)
else:
self.xscale = None
if subsampling_conv_channels == -1:
subsampling_conv_channels = d_model
if subsampling and subsampling_factor > 1:
self.pre_encode = ConvSubsampling(
subsampling=subsampling,
subsampling_factor=subsampling_factor,
feat_in=feat_in,
feat_out=d_model,
conv_channels=subsampling_conv_channels,
activation=nn.ReLU(),
)
self._feat_out = d_model
else:
self._feat_out = d_model
self.pre_encode = nn.Linear(feat_in, d_model)
if not untie_biases and self_attention_model == "rel_pos":
d_head = d_model // n_heads
pos_bias_u = nn.Parameter(torch.Tensor(n_heads, d_head))
pos_bias_v = nn.Parameter(torch.Tensor(n_heads, d_head))
nn.init.zeros_(pos_bias_u)
nn.init.zeros_(pos_bias_v)
else:
pos_bias_u = None
pos_bias_v = None
if self_attention_model == "rel_pos":
self.pos_enc = RelPositionalEncoding(
d_model=d_model,
dropout_rate=dropout,
max_len=pos_emb_max_len,
xscale=self.xscale,
dropout_rate_emb=dropout_emb,
)
elif self_attention_model == "abs_pos":
pos_bias_u = None
pos_bias_v = None
self.pos_enc = PositionalEncoding(
d_model=d_model, dropout_rate=dropout, max_len=pos_emb_max_len, xscale=self.xscale
)
else:
raise ValueError(f"Not valid self_attention_model: '{self_attention_model}'!")
self.layers = nn.ModuleList()
for i in range(n_layers):
layer = ConformerLayer(
d_model=d_model,
d_ff=d_ff,
self_attention_model=self_attention_model,
n_heads=n_heads,
conv_kernel_size=conv_kernel_size,
dropout=dropout,
dropout_att=dropout_att,
pos_bias_u=pos_bias_u,
pos_bias_v=pos_bias_v,
)
self.layers.append(layer)
if feat_out > 0 and feat_out != self.output_dim:
self.out_proj = nn.Linear(self.feat_out, feat_out)
self._feat_out = feat_out
else:
self.out_proj = None
self._feat_out = d_model
@typecheck()
def forward(self, audio_signal, length=None):
if length is None:
length = torch.tensor(audio_signal.size(-1)).repeat(audio_signal.size(0)).to(audio_signal)
audio_signal = torch.transpose(audio_signal, 1, 2)
if isinstance(self.pre_encode, ConvSubsampling):
audio_signal, length = self.pre_encode(audio_signal, length)
else:
audio_signal = self.pre_encode(audio_signal)
audio_signal, pos_emb = self.pos_enc(audio_signal)
bs, xmax, idim = audio_signal.size()
# Create the self-attention and padding masks
pad_mask = self.make_pad_mask(length, max_time=xmax, device=audio_signal.device)
att_mask = pad_mask.unsqueeze(1).repeat([1, xmax, 1])
att_mask = att_mask & att_mask.transpose(1, 2)
if self.att_context_size[0] >= 0:
att_mask = att_mask.triu(diagonal=-self.att_context_size[0])
if self.att_context_size[1] >= 0:
att_mask = att_mask.tril(diagonal=self.att_context_size[1])
att_mask = ~att_mask
pad_mask = ~pad_mask
for lth, layer in enumerate(self.layers):
audio_signal = layer(x=audio_signal, att_mask=att_mask, pos_emb=pos_emb, pad_mask=pad_mask)
if self.out_proj is not None:
audio_signal = self.out_proj(audio_signal)
audio_signal = torch.transpose(audio_signal, 1, 2)
return audio_signal, length
@staticmethod
def make_pad_mask(seq_lens, max_time, device=None):
"""Make masking for padding."""
bs = seq_lens.size(0)
seq_range = torch.arange(0, max_time, dtype=torch.int32)
seq_range_expand = seq_range.unsqueeze(0).expand(bs, max_time)
seq_lens = seq_lens.type(seq_range_expand.dtype).to(seq_range_expand.device)
seq_length_expand = seq_lens.unsqueeze(-1)
mask = seq_range_expand < seq_length_expand
if device:
mask = mask.to(device)
return mask
@@ -0,0 +1,504 @@
# Copyright (c) 2020, 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.
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import List, Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from omegaconf import MISSING, ListConfig, OmegaConf
from nemo.collections.asr.parts.submodules.jasper import (
JasperBlock,
MaskedConv1d,
StatsPoolLayer,
init_weights,
jasper_activations,
)
from nemo.core.classes.common import typecheck
from nemo.core.classes.exportable import Exportable
from nemo.core.classes.module import NeuralModule
from nemo.core.neural_types import (
AcousticEncodedRepresentation,
LengthsType,
LogitsType,
LogprobsType,
NeuralType,
SpectrogramType,
)
from nemo.utils import logging
__all__ = ['ConvASRDecoder', 'ConvASREncoder', 'ConvASRDecoderClassification']
class ConvASREncoder(NeuralModule, Exportable):
"""
Convolutional encoder for ASR models. With this class you can implement JasperNet and QuartzNet models.
Based on these papers:
https://arxiv.org/pdf/1904.03288.pdf
https://arxiv.org/pdf/1910.10261.pdf
"""
def _prepare_for_export(self, **kwargs):
m_count = 0
for m in self.modules():
if isinstance(m, MaskedConv1d):
m.use_mask = False
m_count += 1
Exportable._prepare_for_export(self, **kwargs)
logging.warning(f"Turned off {m_count} masked convolutions")
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
input_example = torch.randn(16, self._feat_in, 256).to(next(self.parameters()).device)
return tuple([input_example])
@property
def disabled_deployment_input_names(self):
"""Implement this method to return a set of input names disabled for export"""
return set(["length"])
@property
def disabled_deployment_output_names(self):
"""Implement this method to return a set of output names disabled for export"""
return set(["encoded_lengths"])
def save_to(self, save_path: str):
pass
@classmethod
def restore_from(cls, restore_path: str):
pass
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return OrderedDict(
{
"audio_signal": NeuralType(('B', 'D', 'T'), SpectrogramType()),
"length": NeuralType(tuple('B'), LengthsType()),
}
)
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return OrderedDict(
{
"outputs": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
}
)
def __init__(
self,
jasper,
activation: str,
feat_in: int,
normalization_mode: str = "batch",
residual_mode: str = "add",
norm_groups: int = -1,
conv_mask: bool = True,
frame_splicing: int = 1,
init_mode: Optional[str] = 'xavier_uniform',
quantize: bool = False,
):
super().__init__()
if isinstance(jasper, ListConfig):
jasper = OmegaConf.to_container(jasper)
activation = jasper_activations[activation]()
# If the activation can be executed in place, do so.
if hasattr(activation, 'inplace'):
activation.inplace = True
feat_in = feat_in * frame_splicing
self._feat_in = feat_in
residual_panes = []
encoder_layers = []
self.dense_residual = False
for lcfg in jasper:
dense_res = []
if lcfg.get('residual_dense', False):
residual_panes.append(feat_in)
dense_res = residual_panes
self.dense_residual = True
groups = lcfg.get('groups', 1)
separable = lcfg.get('separable', False)
heads = lcfg.get('heads', -1)
residual_mode = lcfg.get('residual_mode', residual_mode)
se = lcfg.get('se', False)
se_reduction_ratio = lcfg.get('se_reduction_ratio', 8)
se_context_window = lcfg.get('se_context_size', -1)
se_interpolation_mode = lcfg.get('se_interpolation_mode', 'nearest')
kernel_size_factor = lcfg.get('kernel_size_factor', 1.0)
stride_last = lcfg.get('stride_last', False)
future_context = lcfg.get('future_context', -1)
encoder_layers.append(
JasperBlock(
feat_in,
lcfg['filters'],
repeat=lcfg['repeat'],
kernel_size=lcfg['kernel'],
stride=lcfg['stride'],
dilation=lcfg['dilation'],
dropout=lcfg['dropout'],
residual=lcfg['residual'],
groups=groups,
separable=separable,
heads=heads,
residual_mode=residual_mode,
normalization=normalization_mode,
norm_groups=norm_groups,
activation=activation,
residual_panes=dense_res,
conv_mask=conv_mask,
se=se,
se_reduction_ratio=se_reduction_ratio,
se_context_window=se_context_window,
se_interpolation_mode=se_interpolation_mode,
kernel_size_factor=kernel_size_factor,
stride_last=stride_last,
future_context=future_context,
quantize=quantize,
)
)
feat_in = lcfg['filters']
self._feat_out = feat_in
self.encoder = torch.nn.Sequential(*encoder_layers)
self.apply(lambda x: init_weights(x, mode=init_mode))
@typecheck()
def forward(self, audio_signal, length=None):
s_input, length = self.encoder(([audio_signal], length))
if length is None:
return s_input[-1]
return s_input[-1], length
class ConvASRDecoder(NeuralModule, Exportable):
"""Simple ASR Decoder for use with CTC-based models such as JasperNet and QuartzNet
Based on these papers:
https://arxiv.org/pdf/1904.03288.pdf
https://arxiv.org/pdf/1910.10261.pdf
https://arxiv.org/pdf/2005.04290.pdf
"""
def save_to(self, save_path: str):
pass
@classmethod
def restore_from(cls, restore_path: str):
pass
@property
def input_types(self):
return OrderedDict({"encoder_output": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation())})
@property
def output_types(self):
return OrderedDict({"logprobs": NeuralType(('B', 'T', 'D'), LogprobsType())})
def __init__(self, feat_in, num_classes, init_mode="xavier_uniform", vocabulary=None):
super().__init__()
if vocabulary is not None:
if num_classes != len(vocabulary):
raise ValueError(
f"If vocabulary is specified, it's length should be equal to the num_classes. Instead got: num_classes={num_classes} and len(vocabulary)={len(vocabulary)}"
)
self.__vocabulary = vocabulary
self._feat_in = feat_in
# Add 1 for blank char
self._num_classes = num_classes + 1
self.decoder_layers = torch.nn.Sequential(
torch.nn.Conv1d(self._feat_in, self._num_classes, kernel_size=1, bias=True)
)
self.apply(lambda x: init_weights(x, mode=init_mode))
@typecheck()
def forward(self, encoder_output):
return torch.nn.functional.log_softmax(self.decoder_layers(encoder_output).transpose(1, 2), dim=-1)
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
bs = 8
seq = 64
input_example = torch.randn(bs, self._feat_in, seq).to(next(self.parameters()).device)
return tuple([input_example])
def _prepare_for_export(self, **kwargs):
m_count = 0
for m in self.modules():
if type(m).__name__ == "MaskedConv1d":
m.use_mask = False
m_count += 1
if m_count > 0:
logging.warning(f"Turned off {m_count} masked convolutions")
Exportable._prepare_for_export(self, **kwargs)
@property
def vocabulary(self):
return self.__vocabulary
@property
def num_classes_with_blank(self):
return self._num_classes
class ConvASRDecoderClassification(NeuralModule, Exportable):
"""Simple ASR Decoder for use with classification models such as JasperNet and QuartzNet
Based on these papers:
https://arxiv.org/pdf/2005.04290.pdf
"""
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
input_example = torch.randn(16, self._feat_in, 128).to(next(self.parameters()).device)
return tuple([input_example])
@property
def input_types(self):
return OrderedDict({"encoder_output": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation())})
@property
def output_types(self):
return OrderedDict({"logits": NeuralType(('B', 'D'), LogitsType())})
def __init__(
self,
feat_in: int,
num_classes: int,
init_mode: Optional[str] = "xavier_uniform",
return_logits: bool = True,
pooling_type='avg',
):
super().__init__()
self._feat_in = feat_in
self._return_logits = return_logits
self._num_classes = num_classes
if pooling_type == 'avg':
self.pooling = torch.nn.AdaptiveAvgPool1d(1)
elif pooling_type == 'max':
self.pooling = torch.nn.AdaptiveMaxPool1d(1)
else:
raise ValueError('Pooling type chosen is not valid. Must be either `avg` or `max`')
self.decoder_layers = torch.nn.Sequential(torch.nn.Linear(self._feat_in, self._num_classes, bias=True))
self.apply(lambda x: init_weights(x, mode=init_mode))
@typecheck()
def forward(self, encoder_output):
batch, in_channels, timesteps = encoder_output.size()
encoder_output = self.pooling(encoder_output).view(batch, in_channels) # [B, C]
logits = self.decoder_layers(encoder_output) # [B, num_classes]
if self._return_logits:
return logits
return torch.nn.functional.softmax(logits, dim=-1)
@property
def num_classes(self):
return self._num_classes
class SpeakerDecoder(NeuralModule, Exportable):
"""
Speaker Decoder creates the final neural layers that maps from the outputs
of Jasper Encoder to the embedding layer followed by speaker based softmax loss.
Args:
feat_in (int): Number of channels being input to this module
num_classes (int): Number of unique speakers in dataset
emb_sizes (list) : shapes of intermediate embedding layers (we consider speaker embbeddings from 1st of this layers)
Defaults to [1024,1024]
pool_mode (str) : Pooling stratergy type. options are 'gram','xvector','superVector'.
Defaults to 'xvector'
init_mode (str): Describes how neural network parameters are
initialized. Options are ['xavier_uniform', 'xavier_normal',
'kaiming_uniform','kaiming_normal'].
Defaults to "xavier_uniform".
"""
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
input_example = torch.randn(16, self.input_feat_in, 256).to(next(self.parameters()).device)
return tuple([input_example])
@property
def input_types(self):
return OrderedDict({"encoder_output": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation())})
@property
def output_types(self):
return OrderedDict(
{
"logits": NeuralType(('B', 'D'), LogitsType()),
"embs": NeuralType(('B', 'D'), AcousticEncodedRepresentation()),
}
)
def __init__(
self, feat_in, num_classes, emb_sizes=None, pool_mode='xvector', angular=False, init_mode="xavier_uniform",
):
super().__init__()
self.angular = angular
self.emb_id = 2
if self.angular:
bias = False
else:
bias = True
if type(emb_sizes) is str:
emb_sizes = emb_sizes.split(',')
elif type(emb_sizes) is int:
emb_sizes = [emb_sizes]
else:
emb_sizes = [512, 512]
self.input_feat_in = feat_in
self._num_classes = num_classes
self._pooling = StatsPoolLayer(feat_in=feat_in, pool_mode=pool_mode)
self._feat_in = self._pooling.feat_in
shapes = [self._feat_in]
for size in emb_sizes:
shapes.append(int(size))
emb_layers = []
for shape_in, shape_out in zip(shapes[:-1], shapes[1:]):
layer = self.affineLayer(shape_in, shape_out, learn_mean=False)
emb_layers.append(layer)
self.emb_layers = nn.ModuleList(emb_layers)
self.final = nn.Linear(shapes[-1], self._num_classes, bias=bias)
self.apply(lambda x: init_weights(x, mode=init_mode))
def affineLayer(self, inp_shape, out_shape, learn_mean=True):
layer = nn.Sequential(
nn.Linear(inp_shape, out_shape),
nn.BatchNorm1d(out_shape, affine=learn_mean, track_running_stats=True),
nn.ReLU(),
)
return layer
@typecheck()
def forward(self, encoder_output):
pool = self._pooling(encoder_output)
embs = []
for layer in self.emb_layers:
pool, emb = layer(pool), layer[: self.emb_id](pool)
embs.append(emb)
if self.angular:
for W in self.final.parameters():
W = F.normalize(W, p=2, dim=1)
pool = F.normalize(pool, p=2, dim=1)
out = self.final(pool)
return out, embs[-1]
@dataclass
class JasperEncoderConfig:
filters: int = MISSING
repeat: int = MISSING
kernel: List[int] = MISSING
stride: List[int] = MISSING
dilation: List[int] = MISSING
dropout: float = MISSING
residual: bool = MISSING
# Optional arguments
groups: int = 1
separable: bool = False
heads: int = -1
residual_mode: str = "add"
residual_dense: bool = False
se: bool = False
se_reduction_ratio: int = 8
se_context_size: int = -1
se_interpolation_mode: str = 'nearest'
kernel_size_factor: float = 1.0
stride_last: bool = False
@dataclass
class ConvASREncoderConfig:
_target_: str = 'nemo.collections.asr.modules.ConvASREncoder'
jasper: Optional[JasperEncoderConfig] = field(default_factory=list)
activation: str = MISSING
feat_in: int = MISSING
normalization_mode: str = "batch"
residual_mode: str = "add"
norm_groups: int = -1
conv_mask: bool = True
frame_splicing: int = 1
init_mode: Optional[str] = "xavier_uniform"
@dataclass
class ConvASRDecoderConfig:
_target_: str = 'nemo.collections.asr.modules.ConvASRDecoder'
feat_in: int = MISSING
num_classes: int = MISSING
init_mode: Optional[str] = "xavier_uniform"
vocabulary: Optional[List[str]] = field(default_factory=list)
@dataclass
class ConvASRDecoderClassificationConfig:
_target_: str = 'nemo.collections.asr.modules.ConvASRDecoderClassification'
feat_in: int = MISSING
num_classes: int = MISSING
init_mode: Optional[str] = "xavier_uniform"
return_logits: bool = True
pooling_type: str = 'avg'
@@ -0,0 +1,95 @@
# Copyright (c) 2020, 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.
from collections import OrderedDict
import torch
import torch.nn as nn
from nemo.core.classes.common import typecheck
from nemo.core.classes.exportable import Exportable
from nemo.core.classes.module import NeuralModule
from nemo.core.neural_types import AcousticEncodedRepresentation, LogprobsType, NeuralType
__all__ = ['LSTMDecoder']
class LSTMDecoder(NeuralModule, Exportable):
"""
Simple LSTM Decoder for ASR models
Args:
feat_in (int): size of the input features
num_classes (int): the size of the vocabulary
lstm_hidden_size (int): hidden size of the LSTM layers
vocabulary (vocab): The vocabulary
bidirectional (bool): default is False. Whether LSTMs are bidirectional or not
num_layers (int): default is 1. Number of LSTM layers stacked
"""
@property
def input_types(self):
return OrderedDict({"encoder_output": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation())})
@property
def output_types(self):
return OrderedDict({"logprobs": NeuralType(('B', 'T', 'D'), LogprobsType())})
def __init__(self, feat_in, num_classes, lstm_hidden_size, vocabulary=None, bidirectional=False, num_layers=1):
super().__init__()
if vocabulary is not None:
if num_classes != len(vocabulary):
raise ValueError(
f"If vocabulary is specified, it's length should be equal to the num_classes. "
f"Instead got: num_classes={num_classes} and len(vocabulary)={len(vocabulary)}"
)
self.__vocabulary = vocabulary
self._feat_in = feat_in
# Add 1 for blank char
self._num_classes = num_classes + 1
self.lstm_layer = nn.LSTM(
input_size=feat_in,
hidden_size=lstm_hidden_size,
num_layers=num_layers,
batch_first=True,
bidirectional=bidirectional,
)
self.linear_layer = torch.nn.Linear(in_features=lstm_hidden_size, out_features=self._num_classes)
@typecheck()
def forward(self, encoder_output):
output = encoder_output.transpose(1, 2)
output, _ = self.lstm_layer(output)
output = self.linear_layer(output)
return torch.nn.functional.log_softmax(output, dim=-1)
def input_example(self):
"""
Generates input examples for tracing etc.
Returns:
A tuple of input examples.
"""
bs = 8
seq = 64
input_example = torch.randn(bs, self._feat_in, seq).to(next(self.parameters()).device)
return tuple([input_example])
@property
def vocabulary(self):
return self.__vocabulary
@property
def num_classes_with_blank(self):
return self._num_classes
+958
View File
@@ -0,0 +1,958 @@
# Copyright (c) 2020, 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.
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
#
# 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.
from typing import Any, Dict, List, Optional, Tuple, Union
import torch
from nemo.collections.asr.modules import rnnt_abstract
from nemo.collections.asr.parts.utils import rnnt_utils
from nemo.collections.common.parts import rnn
from nemo.core.classes import typecheck
from nemo.core.neural_types import (
AcousticEncodedRepresentation,
ElementType,
EmbeddedTextType,
LabelsType,
LengthsType,
LogprobsType,
LossType,
NeuralType,
)
from nemo.utils import logging
class RNNTDecoder(rnnt_abstract.AbstractRNNTDecoder):
"""A Recurrent Neural Network Transducer Decoder / Prediction Network (RNN-T Prediction Network).
An RNN-T Decoder/Prediction network, comprised of a stateful LSTM model.
Args:
prednet: A dict-like object which contains the following key-value pairs.
pred_hidden: int specifying the hidden dimension of the prediction net.
pred_rnn_layers: int specifying the number of rnn layers.
Optionally, it may also contain the following:
forget_gate_bias: float, set by default to 1.0, which constructs a forget gate
initialized to 1.0.
Reference:
[An Empirical Exploration of Recurrent Network Architectures](http://proceedings.mlr.press/v37/jozefowicz15.pdf)
t_max: int value, set to None by default. If an int is specified, performs Chrono Initialization
of the LSTM network, based on the maximum number of timesteps `t_max` expected during the course
of training.
Reference:
[Can recurrent neural networks warp time?](https://openreview.net/forum?id=SJcKhk-Ab)
weights_init_scale: Float scale of the weights after initialization. Setting to lower than one
sometimes helps reduce variance between runs.
hidden_hidden_bias_scale: Float scale for the hidden-to-hidden bias scale. Set to 0.0 for
the default behaviour.
dropout: float, set to 0.0 by default. Optional dropout applied at the end of the final LSTM RNN layer.
vocab_size: int, specifying the vocabulary size of the embedding layer of the Prediction network,
excluding the RNNT blank token.
normalization_mode: Can be either None, 'batch' or 'layer'. By default, is set to None.
Defines the type of normalization applied to the RNN layer.
random_state_sampling: bool, set to False by default. When set, provides normal-distribution
sampled state tensors instead of zero tensors during training.
Reference:
[Recognizing long-form speech using streaming end-to-end models](https://arxiv.org/abs/1910.11455)
blank_as_pad: bool, set to True by default. When set, will add a token to the Embedding layer of this
prediction network, and will treat this token as a pad token. In essence, the RNNT pad token will
be treated as a pad token, and the embedding layer will return a zero tensor for this token.
It is set by default as it enables various batch optimizations required for batched beam search.
Therefore, it is not recommended to disable this flag.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"targets": NeuralType(('B', 'T'), LabelsType()),
"target_length": NeuralType(tuple('B'), LengthsType()),
"states": NeuralType(('D', 'B', 'D'), ElementType(), optional=True),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {
"outputs": NeuralType(('B', 'D', 'T'), EmbeddedTextType()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
}
def __init__(
self,
prednet: Dict[str, Any],
vocab_size: int,
normalization_mode: Optional[str] = None,
random_state_sampling: bool = False,
blank_as_pad: bool = True,
):
# Required arguments
self.pred_hidden = prednet['pred_hidden']
self.pred_rnn_layers = prednet["pred_rnn_layers"]
self.blank_idx = vocab_size
# Initialize the model (blank token increases vocab size by 1)
super().__init__(vocab_size=vocab_size, blank_idx=self.blank_idx, blank_as_pad=blank_as_pad)
# Optional arguments
forget_gate_bias = prednet.get('forget_gate_bias', 1.0)
t_max = prednet.get('t_max', None)
weights_init_scale = prednet.get('weights_init_scale', 1.0)
hidden_hidden_bias_scale = prednet.get('hidden_hidden_bias_scale', 0.0)
dropout = prednet.get('dropout', 0.0)
self.random_state_sampling = random_state_sampling
self.prediction = self._predict(
vocab_size=vocab_size, # add 1 for blank symbol
pred_n_hidden=self.pred_hidden,
pred_rnn_layers=self.pred_rnn_layers,
forget_gate_bias=forget_gate_bias,
t_max=t_max,
norm=normalization_mode,
weights_init_scale=weights_init_scale,
hidden_hidden_bias_scale=hidden_hidden_bias_scale,
dropout=dropout,
)
@typecheck()
def forward(self, targets, target_length, states=None):
# y: (B, U)
y = rnn.label_collate(targets)
# state maintenance is unnecessary during training forward call
# to get state, use .predict() method.
g, _ = self.predict(y, state=states, add_sos=True) # (B, U, D)
g = g.transpose(1, 2) # (B, D, U)
return g, target_length
def predict(
self,
y: Optional[torch.Tensor] = None,
state: Optional[List[torch.Tensor]] = None,
add_sos: bool = True,
batch_size: Optional[int] = None,
) -> (torch.Tensor, List[torch.Tensor]):
"""
Stateful prediction of scores and state for a (possibly null) tokenset.
This method takes various cases into consideration :
- No token, no state - used for priming the RNN
- No token, state provided - used for blank token scoring
- Given token, states - used for scores + new states
Here:
B - batch size
U - label length
H - Hidden dimension size of RNN
L - Number of RNN layers
Args:
y: Optional torch tensor of shape [B, U] of dtype long which will be passed to the Embedding.
If None, creates a zero tensor of shape [B, 1, H] which mimics output of pad-token on Embedding.
state: An optional list of states for the RNN. Eg: For LSTM, it is the state list length is 2.
Each state must be a tensor of shape [L, B, H].
If None, and during training mode and `random_state_sampling` is set, will sample a
normal distribution tensor of the above shape. Otherwise, None will be passed to the RNN.
add_sos: bool flag, whether a zero vector describing a "start of signal" token should be
prepended to the above "y" tensor. When set, output size is (B, U + 1, H).
batch_size: An optional int, specifying the batch size of the `y` tensor.
Can be infered if `y` and `state` is None. But if both are None, then batch_size cannot be None.
Returns:
A tuple (g, hid) such that -
If add_sos is False:
g: (B, U, H)
hid: (h, c) where h is the final sequence hidden state and c is the final cell state:
h (tensor), shape (L, B, H)
c (tensor), shape (L, B, H)
If add_sos is True:
g: (B, U + 1, H)
hid: (h, c) where h is the final sequence hidden state and c is the final cell state:
h (tensor), shape (L, B, H)
c (tensor), shape (L, B, H)
"""
# Get device and dtype of current module
_p = next(self.parameters())
device = _p.device
dtype = _p.dtype
# If y is not None, it is of shape [B, U] with dtype long.
if y is not None:
if y.device != device:
y = y.to(device)
# (B, U) -> (B, U, H)
y = self.prediction["embed"](y)
else:
# Y is not provided, assume zero tensor with shape [B, 1, H] is required
# Emulates output of embedding of pad token.
if batch_size is None:
B = 1 if state is None else state[0].size(1)
else:
B = batch_size
y = torch.zeros((B, 1, self.pred_hidden), device=device, dtype=dtype)
# Prepend blank "start of sequence" symbol (zero tensor)
if add_sos:
B, U, H = y.shape
start = torch.zeros((B, 1, H), device=y.device, dtype=y.dtype)
y = torch.cat([start, y], dim=1).contiguous() # (B, U + 1, H)
else:
start = None # makes del call later easier
# If in training mode, and random_state_sampling is set,
# initialize state to random normal distribution tensor.
if state is None:
if self.random_state_sampling and self.training:
state = self.initialize_state(y)
# Forward step through RNN
y = y.transpose(0, 1) # (U + 1, B, H)
g, hid = self.prediction["dec_rnn"](y, state)
g = g.transpose(0, 1) # (B, U + 1, H)
del y, start, state
return g, hid
def _predict(
self,
vocab_size,
pred_n_hidden,
pred_rnn_layers,
forget_gate_bias,
t_max,
norm,
weights_init_scale,
hidden_hidden_bias_scale,
dropout,
):
"""
Prepare the trainable parameters of the Prediction Network.
Args:
vocab_size: Vocab size (excluding the blank token).
pred_n_hidden: Hidden size of the RNNs.
pred_rnn_layers: Number of RNN layers.
forget_gate_bias: Whether to perform unit forget gate bias.
t_max: Whether to perform Chrono LSTM init.
norm: Type of normalization to perform in RNN.
weights_init_scale: Float scale of the weights after initialization. Setting to lower than one
sometimes helps reduce variance between runs.
hidden_hidden_bias_scale: Float scale for the hidden-to-hidden bias scale. Set to 0.0 for
the default behaviour.
dropout: Whether to apply dropout to RNN.
"""
if self.blank_as_pad:
embed = torch.nn.Embedding(vocab_size + 1, pred_n_hidden, padding_idx=self.blank_idx)
else:
embed = torch.nn.Embedding(vocab_size, pred_n_hidden)
layers = torch.nn.ModuleDict(
{
"embed": embed,
"dec_rnn": rnn.rnn(
input_size=pred_n_hidden,
hidden_size=pred_n_hidden,
num_layers=pred_rnn_layers,
norm=norm,
forget_gate_bias=forget_gate_bias,
t_max=t_max,
dropout=dropout,
weights_init_scale=weights_init_scale,
hidden_hidden_bias_scale=hidden_hidden_bias_scale,
),
}
)
return layers
def initialize_state(self, y: torch.Tensor) -> List[torch.Tensor]:
"""
Initialize the state of the RNN layers, with same dtype and device as input `y`.
Args:
y: A torch.Tensor whose device the generated states will be placed on.
Returns:
List of torch.Tensor, each of shape [L, B, H], where
L = Number of RNN layers
B = Batch size
H = Hidden size of RNN.
"""
batch = y.size(0)
if self.random_state_sampling and self.training:
state = [
torch.randn(self.pred_rnn_layers, batch, self.pred_hidden, dtype=y.dtype, device=y.device),
torch.randn(self.pred_rnn_layers, batch, self.pred_hidden, dtype=y.dtype, device=y.device),
]
else:
state = [
torch.zeros(self.pred_rnn_layers, batch, self.pred_hidden, dtype=y.dtype, device=y.device),
torch.zeros(self.pred_rnn_layers, batch, self.pred_hidden, dtype=y.dtype, device=y.device),
]
return state
def score_hypothesis(
self, hypothesis: rnnt_utils.Hypothesis, cache: Dict[Tuple[int], Any]
) -> (torch.Tensor, List[torch.Tensor], torch.Tensor):
"""
Similar to the predict() method, instead this method scores a Hypothesis during beam search.
Hypothesis is a dataclass representing one hypothesis in a Beam Search.
Args:
hypothesis: Refer to rnnt_utils.Hypothesis.
cache: Dict which contains a cache to avoid duplicate computations.
Returns:
Returns a tuple (y, states, lm_token) such that:
y is a torch.Tensor of shape [1, 1, H] representing the score of the last token in the Hypothesis.
state is a list of RNN states, each of shape [L, 1, H].
lm_token is the final integer token of the hypothesis.
"""
if hypothesis.dec_state is not None:
device = hypothesis.dec_state[0].device
else:
_p = next(self.parameters())
device = _p.device
# parse "blank" tokens in hypothesis
if len(hypothesis.y_sequence) > 0 and hypothesis.y_sequence[-1] == self.blank_idx:
blank_state = True
else:
blank_state = False
# Convert last token of hypothesis to torch.Tensor
target = torch.full([1, 1], fill_value=hypothesis.y_sequence[-1], device=device, dtype=torch.long)
lm_token = target[:, -1] # [1]
# Convert current hypothesis into a tuple to preserve in cache
sequence = tuple(hypothesis.y_sequence)
if sequence in cache:
y, new_state = cache[sequence]
else:
# Obtain score for target token and new states
if blank_state:
y, new_state = self.predict(None, state=None, add_sos=False, batch_size=1) # [1, 1, H]
else:
y, new_state = self.predict(
target, state=hypothesis.dec_state, add_sos=False, batch_size=1
) # [1, 1, H]
y = y[:, -1:, :] # Extract just last state : [1, 1, H]
cache[sequence] = (y, new_state)
return y, new_state, lm_token
def batch_score_hypothesis(
self, hypotheses: List[rnnt_utils.Hypothesis], cache: Dict[Tuple[int], Any], batch_states: List[torch.Tensor]
) -> (torch.Tensor, List[torch.Tensor], torch.Tensor):
"""
Used for batched beam search algorithms. Similar to score_hypothesis method.
Args:
hypothesis: List of Hypotheses. Refer to rnnt_utils.Hypothesis.
cache: Dict which contains a cache to avoid duplicate computations.
batch_states: List of torch.Tensor which represent the states of the RNN for this batch.
Each state is of shape [L, B, H]
Returns:
Returns a tuple (b_y, b_states, lm_tokens) such that:
b_y is a torch.Tensor of shape [B, 1, H] representing the scores of the last tokens in the Hypotheses.
b_state is a list of list of RNN states, each of shape [L, B, H].
Represented as B x List[states].
lm_token is a list of the final integer tokens of the hypotheses in the batch.
"""
final_batch = len(hypotheses)
if final_batch == 0:
raise ValueError("No hypotheses was provided for the batch!")
_p = next(self.parameters())
device = _p.device
dtype = _p.dtype
tokens = []
process = []
done = [None for _ in range(final_batch)]
# For each hypothesis, cache the last token of the sequence and the current states
for i, hyp in enumerate(hypotheses):
sequence = tuple(hyp.y_sequence)
if sequence in cache:
done[i] = cache[sequence]
else:
tokens.append(hyp.y_sequence[-1])
process.append((sequence, hyp.dec_state))
if process:
batch = len(process)
# convert list of tokens to torch.Tensor, then reshape.
tokens = torch.tensor(tokens, device=device, dtype=torch.long).view(batch, -1)
dec_states = self.initialize_state(tokens.to(dtype=dtype)) # [L, B, H]
dec_states = self.batch_initialize_states(dec_states, [d_state for seq, d_state in process])
y, dec_states = self.predict(
tokens, state=dec_states, add_sos=False, batch_size=batch
) # [B, 1, H], List([L, 1, H])
# Update done states and cache shared by entire batch.
j = 0
for i in range(final_batch):
if done[i] is None:
# Select sample's state from the batch state list
new_state = self.batch_select_state(dec_states, j)
# Cache [1, H] scores of the current y_j, and its corresponding state
done[i] = (y[j], new_state)
cache[process[j][0]] = (y[j], new_state)
j += 1
# Set the incoming batch states with the new states obtained from `done`.
batch_states = self.batch_initialize_states(batch_states, [d_state for y_j, d_state in done])
# Create batch of all output scores
# List[1, 1, H] -> [B, 1, H]
batch_y = torch.stack([y_j for y_j, d_state in done])
# Extract the last tokens from all hypotheses and convert to a tensor
lm_tokens = torch.tensor([h.y_sequence[-1] for h in hypotheses], device=device, dtype=torch.long).view(
final_batch
)
return batch_y, batch_states, lm_tokens
def batch_initialize_states(self, batch_states: List[torch.Tensor], decoder_states: List[List[torch.Tensor]]):
"""
Create batch of decoder states.
Args:
batch_states (list): batch of decoder states
([L x (B, H)], [L x (B, H)])
decoder_states (list of list): list of decoder states
[B x ([L x (1, H)], [L x (1, H)])]
Returns:
batch_states (tuple): batch of decoder states
([L x (B, H)], [L x (B, H)])
"""
# LSTM has 2 states
for layer in range(self.pred_rnn_layers):
for state_id in range(len(batch_states)):
batch_states[state_id][layer] = torch.stack([s[state_id][layer] for s in decoder_states])
return batch_states
def batch_select_state(self, batch_states: List[torch.Tensor], idx: int) -> List[List[torch.Tensor]]:
"""Get decoder state from batch of states, for given id.
Args:
batch_states (list): batch of decoder states
([L x (B, H)], [L x (B, H)])
idx (int): index to extract state from batch of states
Returns:
(tuple): decoder states for given id
([L x (1, H)], [L x (1, H)])
"""
state_list = []
for state_id in range(len(batch_states)):
states = [batch_states[state_id][layer][idx] for layer in range(self.pred_rnn_layers)]
state_list.append(states)
return state_list
class RNNTJoint(rnnt_abstract.AbstractRNNTJoint):
"""A Recurrent Neural Network Transducer Joint Network (RNN-T Joint Network).
An RNN-T Joint network, comprised of a feedforward model.
Args:
jointnet: A dict-like object which contains the following key-value pairs.
encoder_hidden: int specifying the hidden dimension of the encoder net.
pred_hidden: int specifying the hidden dimension of the prediction net.
joint_hidden: int specifying the hidden dimension of the joint net
activation: Activation function used in the joint step. Can be one of
['relu', 'tanh', 'sigmoid'].
Optionally, it may also contain the following:
dropout: float, set to 0.0 by default. Optional dropout applied at the end of the joint net.
num_classes: int, specifying the vocabulary size that the joint network must predict,
excluding the RNNT blank token.
vocabulary: Optional list of strings/tokens that comprise the vocabulary of the joint network.
Unused and kept only for easy access for character based encoding RNNT models.
log_softmax: Optional bool, set to None by default. If set as None, will compute the log_softmax()
based on the value provided.
preserve_memory: Optional bool, set to False by default. If the model crashes due to the memory
intensive joint step, one might try this flag to empty the tensor cache in pytorch.
Warning: This will make the forward-backward pass much slower than normal.
It also might not fix the OOM if the GPU simply does not have enough memory to compute the joint.
experimental_fuse_loss_wer: Optional bool, set to False by default.
NOTE: This is an experimental feature that attempts to trade of compute time for memory preservation.
There may be undetermined effects to convergence behaviour.
Fuses the joint forward, loss forward and
wer forward steps. In doing so, it trades of speed for memory conservation by creating sub-batches
of the provided batch of inputs, and performs Joint forward, loss forward and wer forward (optional),
all on sub-batches, then collates results to be exactly equal to results from the entire batch.
When this flag is set, prior to calling forward, the fields `loss` and `wer` (either one) *must*
be set using the `RNNTJoint.set_loss()` or `RNNTJoint.set_wer()` methods.
Further, when this flag is set, the following argument `fused_batch_size` *must* be provided
as a non negative integer. This value refers to the size of the sub-batch.
When the flag is set, the input and output signature of `forward()` of this method changes.
Input - in addition to `encoder_outputs` (mandatory argument), the following arguments can be provided.
- decoder_outputs (optional). Required if loss computation is required.
- encoder_lengths (required)
- transcripts (optional). Required for wer calculation.
- transcript_lengths (optional). Required for wer calculation.
- compute_wer (bool, default false). Whether to compute WER or not for the fused batch.
Output - instead of the usual `joint` log prob tensor, the following results can be returned.
- loss (optional). Returned if decoder_outputs, transcripts and transript_lengths are not None.
- wer_numerator + wer_denominator (optional). Returned if transcripts, transcripts_lengths are provided
and compute_wer is set.
fused_batch_size: Optional int, required if `fuse_loss_wer` flag is set. Determines the size of the
sub-batches. Should be any value below the actual batch size per GPU.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"encoder_outputs": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
"decoder_outputs": NeuralType(('B', 'D', 'T'), EmbeddedTextType()),
"encoder_lengths": NeuralType(tuple('B'), LengthsType(), optional=True),
"transcripts": NeuralType(('B', 'T'), LabelsType(), optional=True),
"transcript_lengths": NeuralType(tuple('B'), LengthsType(), optional=True),
"compute_wer": NeuralType(optional=True),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
if not self._fuse_loss_wer:
return {
"outputs": NeuralType(('B', 'T', 'D', 'D'), LogprobsType()),
}
else:
return {
"loss": NeuralType(elements_type=LossType(), optional=True),
"wer": NeuralType(elements_type=ElementType(), optional=True),
"wer_numer": NeuralType(elements_type=ElementType(), optional=True),
"wer_denom": NeuralType(elements_type=ElementType(), optional=True),
}
def __init__(
self,
jointnet: Dict[str, Any],
num_classes: int,
vocabulary: Optional[List] = None,
log_softmax: Optional[bool] = None,
preserve_memory: bool = False,
experimental_fuse_loss_wer: bool = False,
fused_batch_size: Optional[int] = None,
):
super().__init__()
self.vocabulary = vocabulary
self._vocab_size = num_classes
self._num_classes = num_classes + 1 # add 1 for blank symbol
self._fuse_loss_wer = experimental_fuse_loss_wer
self._fused_batch_size = fused_batch_size
if experimental_fuse_loss_wer and (fused_batch_size is None):
raise ValueError("If `fuse_loss_wer` is set, then `fused_batch_size` cannot be None!")
if experimental_fuse_loss_wer:
logging.warning(
"\nFused joint step is an experimental technique. Please be aware that it "
"may have unintended side effects!\n"
)
self._loss = None
self._wer = None
# Log softmax should be applied explicitly only for CPU
self.log_softmax = log_softmax
self.preserve_memory = preserve_memory
if preserve_memory:
logging.warning(
"`preserve_memory` was set for the Joint Model. Please be aware this will severely impact "
"the forward-backward step time. It also might not solve OOM issues if the GPU simply "
"does not have enough memory to compute the joint."
)
# Required arguments
self.encoder_hidden = jointnet['encoder_hidden']
self.pred_hidden = jointnet['pred_hidden']
self.joint_hidden = jointnet['joint_hidden']
self.activation = jointnet['activation']
# Optional arguments
dropout = jointnet.get('dropout', 0.0)
self.pred, self.enc, self.joint_net = self._joint_net(
num_classes=self._num_classes, # add 1 for blank symbol
pred_n_hidden=self.pred_hidden,
enc_n_hidden=self.encoder_hidden,
joint_n_hidden=self.joint_hidden,
activation=self.activation,
dropout=dropout,
)
@typecheck()
def forward(
self,
encoder_outputs: torch.Tensor,
decoder_outputs: Optional[torch.Tensor],
encoder_lengths: Optional[torch.Tensor] = None,
transcripts: Optional[torch.Tensor] = None,
transcript_lengths: Optional[torch.Tensor] = None,
compute_wer: bool = False,
) -> Union[torch.Tensor, List[Optional[torch.Tensor]]]:
# encoder = (B, D, T)
# decoder = (B, D, U) if passed, else None
encoder_outputs = encoder_outputs.transpose(1, 2) # (B, T, D)
if decoder_outputs is not None:
decoder_outputs = decoder_outputs.transpose(1, 2) # (B, U, D)
if not self._fuse_loss_wer:
if decoder_outputs is None:
raise ValueError(
"decoder_outputs passed is None, and `fuse_loss_wer` is not set. "
"decoder_outputs can only be None for fused step!"
)
out = self.joint(encoder_outputs, decoder_outputs) # [B, T, U, V + 1]
return out
else:
# At least the loss module must be supplied during fused joint
if self._loss is None or self._wer is None:
raise ValueError("`fuse_loss_wer` flag is set, but `loss` and `wer` modules were not provided! ")
# If fused joint step is required, fused batch size is required as well
if self._fused_batch_size is None:
raise ValueError("If `experimental_fuse_loss_wer` is set, then `fused_batch_size` cannot be None!")
# When using fused joint step, both encoder and transcript lengths must be provided
if (encoder_lengths is None) or (transcript_lengths is None):
raise ValueError(
"`experimental_fuse_loss_wer` is set, therefore encoder and target lengths "
"must be provided as well!"
)
losses = []
wer_numer_list = []
wer_denom_list = []
batch_size = int(encoder_outputs.size(0)) # actual batch size
# Iterate over batch using fused_batch_size steps
for batch_idx in range(0, batch_size, self._fused_batch_size):
begin = batch_idx
end = min(begin + self._fused_batch_size, batch_size)
# Extract the sub batch inputs
# sub_enc = encoder_outputs[begin:end, ...]
# sub_transcripts = transcripts[begin:end, ...]
sub_enc = encoder_outputs.narrow(dim=0, start=begin, length=end - begin)
sub_transcripts = transcripts.narrow(dim=0, start=begin, length=end - begin)
sub_enc_lens = encoder_lengths[begin:end]
sub_transcript_lens = transcript_lengths[begin:end]
# Sub transcripts does not need the full padding of the entire batch
# Therefore reduce the decoder time steps to match
max_sub_enc_length = sub_enc_lens.max()
max_sub_transcript_length = sub_transcript_lens.max()
if decoder_outputs is not None:
# Reduce encoder length to preserve computation
# Encoder: [sub-batch, T, D] -> [sub-batch, T', D]; T' < T
if sub_enc.shape[1] != max_sub_enc_length:
sub_enc = sub_enc.narrow(dim=1, start=0, length=max_sub_enc_length)
# sub_dec = decoder_outputs[begin:end, ...] # [sub-batch, U, D]
sub_dec = decoder_outputs.narrow(dim=0, start=begin, length=end - begin) # [sub-batch, U, D]
# Reduce decoder length to preserve computation
# Decoder: [sub-batch, U, D] -> [sub-batch, U', D]; U' < U
if sub_dec.shape[1] != max_sub_transcript_length + 1:
sub_dec = sub_dec.narrow(dim=1, start=0, length=max_sub_transcript_length + 1)
# Perform joint => [sub-batch, T', U', V + 1]
sub_joint = self.joint(sub_enc, sub_dec)
del sub_dec
# Reduce transcript length to correct alignment
# Transcript: [sub-batch, L] -> [sub-batch, L']; L' <= L
if sub_transcripts.shape[1] != max_sub_transcript_length:
sub_transcripts = sub_transcripts.narrow(dim=1, start=0, length=max_sub_transcript_length)
# Compute sub batch loss
# preserve loss reduction type
loss_reduction = self.loss.reduction
# override loss reduction to sum
self.loss.reduction = None
# compute and preserve loss
loss_batch = self.loss(
log_probs=sub_joint,
targets=sub_transcripts,
input_lengths=sub_enc_lens,
target_lengths=sub_transcript_lens,
)
losses.append(loss_batch)
# reset loss reduction type
self.loss.reduction = loss_reduction
else:
losses = None
# Compute WER for sub batch
if compute_wer:
sub_enc = sub_enc.transpose(1, 2) # [B, T, D] -> [B, D, T]
sub_enc = sub_enc.detach()
sub_transcripts = sub_transcripts.detach()
original_log_prediction = self.wer.log_prediction
if batch_idx == 0:
self.wer.log_prediction = True
else:
self.wer.log_prediction = False
# Compute the wer (with logging for just 1st sub-batch)
self.wer.update(sub_enc, sub_enc_lens, sub_transcripts, sub_transcript_lens)
wer, wer_num, wer_denom = self.wer.compute()
self.wer.reset()
wer_numer_list.append(wer_num)
wer_denom_list.append(wer_denom)
# Reset logging default
self.wer.log_prediction = original_log_prediction
else:
wer = None
del sub_enc, sub_transcripts, sub_enc_lens, sub_transcript_lens
# Collect sub batch loss results
if losses is not None:
losses = torch.cat(losses, 0)
losses = losses.mean() # global batch size average
else:
losses = None
# Collect sub batch wer results
if compute_wer:
wer_num = torch.tensor(wer_numer_list, dtype=torch.long)
wer_denom = torch.tensor(wer_denom_list, dtype=torch.long)
wer_num = wer_num.sum() # global sum of correct words/chars
wer_denom = wer_denom.sum() # global sum of all words/chars
else:
wer_num = None
wer_denom = None
return losses, wer, wer_num, wer_denom
def joint(self, f: torch.Tensor, g: torch.Tensor) -> torch.Tensor:
"""
Compute the joint step of the network.
Here,
B = Batch size
T = Acoustic model timesteps
U = Target sequence length
H1, H2 = Hidden dimensions of the Encoder / Decoder respectively
H = Hidden dimension of the Joint hidden step.
V = Vocabulary size of the Decoder (excluding the RNNT blank token).
NOTE:
The implementation of this model is slightly modified from the original paper.
The original paper proposes the following steps :
(enc, dec) -> Expand + Concat + Sum [B, T, U, H1+H2] -> Forward through joint hidden [B, T, U, H] -- *1
*1 -> Forward through joint final [B, T, U, V + 1].
We instead split the joint hidden into joint_hidden_enc and joint_hidden_dec and act as follows:
enc -> Forward through joint_hidden_enc -> Expand [B, T, 1, H] -- *1
dec -> Forward through joint_hidden_dec -> Expand [B, 1, U, H] -- *2
(*1, *2) -> Sum [B, T, U, H] -> Forward through joint final [B, T, U, V + 1].
Args:
f: Output of the Encoder model. A torch.Tensor of shape [B, T, H1]
g: Output of the Decoder model. A torch.Tensor of shape [B, U, H2]
Returns:
Logits / log softmaxed tensor of shape (B, T, U, V + 1).
"""
# f = [B, T, H1]
f = self.enc(f)
f.unsqueeze_(dim=2) # (B, T, 1, H)
# g = [B, U, H2]
g = self.pred(g)
g.unsqueeze_(dim=1) # (B, 1, U, H)
inp = f + g # [B, T, U, H]
del f, g
res = self.joint_net(inp) # [B, T, U, V + 1]
del inp
if self.preserve_memory:
torch.cuda.empty_cache()
# If log_softmax is automatic
if self.log_softmax is None:
if not res.is_cuda: # Use log softmax only if on CPU
res = res.log_softmax(dim=-1)
else:
if self.log_softmax:
res = res.log_softmax(dim=-1)
return res
def _joint_net(self, num_classes, pred_n_hidden, enc_n_hidden, joint_n_hidden, activation, dropout):
"""
Prepare the trainable modules of the Joint Network
Args:
num_classes: Number of output classes (vocab size) excluding the RNNT blank token.
pred_n_hidden: Hidden size of the prediction network.
enc_n_hidden: Hidden size of the encoder network.
joint_n_hidden: Hidden size of the joint network.
activation: Activation of the joint. Can be one of [relu, tanh, sigmoid]
dropout: Dropout value to apply to joint.
"""
pred = torch.nn.Linear(pred_n_hidden, joint_n_hidden)
enc = torch.nn.Linear(enc_n_hidden, joint_n_hidden)
if activation not in ['relu', 'sigmoid', 'tanh']:
raise ValueError("Unsupported activation for joint step - please pass one of " "[relu, sigmoid, tanh]")
activation = activation.lower()
if activation == 'relu':
activation = torch.nn.ReLU(inplace=True)
elif activation == 'sigmoid':
activation = torch.nn.Sigmoid()
elif activation == 'tanh':
activation = torch.nn.Tanh()
layers = (
[activation]
+ ([torch.nn.Dropout(p=dropout)] if dropout else [])
+ [torch.nn.Linear(joint_n_hidden, num_classes)]
)
return pred, enc, torch.nn.Sequential(*layers)
@property
def num_classes_with_blank(self):
return self._num_classes
@property
def loss(self):
return self._loss
def set_loss(self, loss):
if not self._fuse_loss_wer:
raise ValueError("Attempting to set loss module even though `fuse_loss_wer` is not set!")
self._loss = loss
@property
def wer(self):
return self._wer
def set_wer(self, wer):
if not self._fuse_loss_wer:
raise ValueError("Attempting to set WER module even though `fuse_loss_wer` is not set!")
self._wer = wer
@property
def fuse_loss_wer(self):
return self._fuse_loss_wer
def set_fuse_loss_wer(self, fuse_loss_wer):
self._fuse_loss_wer = fuse_loss_wer
if self._fuse_loss_wer is False:
self._loss = None
self._wer = None
@property
def fused_batch_size(self):
return self._fuse_loss_wer
def set_fused_batch_size(self, fused_batch_size):
self._fused_batch_size = fused_batch_size
@@ -0,0 +1,236 @@
# Copyright (c) 2020, 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.
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple
import torch
from nemo.collections.asr.parts.utils.rnnt_utils import Hypothesis
from nemo.core import NeuralModule
class AbstractRNNTJoint(NeuralModule, ABC):
"""
An abstract RNNT Joint framework, which can possibly integrate with GreedyRNNTInfer and BeamRNNTInfer classes.
Represents the abstract RNNT Joint network, which accepts the acoustic model and prediction network
embeddings in order to compute the joint of the two prior to decoding the output sequence.
"""
@abstractmethod
def joint(self, f: torch.Tensor, g: torch.Tensor) -> torch.Tensor:
"""
Compute the joint step of the network.
Here,
B = Batch size
T = Acoustic model timesteps
U = Target sequence length
H1, H2 = Hidden dimensions of the Encoder / Decoder respectively
H = Hidden dimension of the Joint hidden step.
V = Vocabulary size of the Decoder (excluding the RNNT blank token).
NOTE:
The implementation of this model is slightly modified from the original paper.
The original paper proposes the following steps :
(enc, dec) -> Expand + Concat + Sum [B, T, U, H1+H2] -> Forward through joint hidden [B, T, U, H] -- *1
*1 -> Forward through joint final [B, T, U, V + 1].
We instead split the joint hidden into joint_hidden_enc and joint_hidden_dec and act as follows:
enc -> Forward through joint_hidden_enc -> Expand [B, T, 1, H] -- *1
dec -> Forward through joint_hidden_dec -> Expand [B, 1, U, H] -- *2
(*1, *2) -> Sum [B, T, U, H] -> Forward through joint final [B, T, U, V + 1].
Args:
f: Output of the Encoder model. A torch.Tensor of shape [B, T, H1]
g: Output of the Decoder model. A torch.Tensor of shape [B, U, H2]
Returns:
Logits / log softmaxed tensor of shape (B, T, U, V + 1).
"""
raise NotImplementedError()
@property
def num_classes_with_blank(self):
raise NotImplementedError()
class AbstractRNNTDecoder(NeuralModule, ABC):
"""
An abstract RNNT Decoder framework, which can possibly integrate with GreedyRNNTInfer and BeamRNNTInfer classes.
Represents the abstract RNNT Prediction/Decoder stateful network, which performs autoregressive decoding
in order to construct the output sequence.
Args:
vocab_size: Size of the vocabulary, excluding the RNNT blank token.
blank_idx: Index of the blank token. Can be 0 or size(vocabulary).
blank_as_pad: Bool flag, whether to allocate an additional token in the Embedding layer
of this module in order to treat all RNNT `blank` tokens as pad tokens, thereby letting
the Embedding layer batch tokens more efficiently.
It is mandatory to use this for certain Beam RNNT Infer methods - such as TSD, ALSD.
It is also more efficient to use greedy batch decoding with this flag.
"""
def __init__(self, vocab_size, blank_idx, blank_as_pad):
super().__init__()
self.vocab_size = vocab_size
self.blank_idx = blank_idx # first or last index of vocabulary
self.blank_as_pad = blank_as_pad
if blank_idx not in [0, vocab_size]:
raise ValueError("`blank_idx` must be either 0 or the final token of the vocabulary")
@abstractmethod
def predict(
self,
y: Optional[torch.Tensor] = None,
state: Optional[torch.Tensor] = None,
add_sos: bool = False,
batch_size: Optional[int] = None,
) -> (torch.Tensor, List[torch.Tensor]):
"""
Stateful prediction of scores and state for a (possibly null) tokenset.
This method takes various cases into consideration :
- No token, no state - used for priming the RNN
- No token, state provided - used for blank token scoring
- Given token, states - used for scores + new states
Here:
B - batch size
U - label length
H - Hidden dimension size of RNN
L - Number of RNN layers
Args:
y: Optional torch tensor of shape [B, U] of dtype long which will be passed to the Embedding.
If None, creates a zero tensor of shape [B, 1, H] which mimics output of pad-token on Embedding.
state: An optional list of states for the RNN. Eg: For LSTM, it is the state list length is 2.
Each state must be a tensor of shape [L, B, H].
If None, and during training mode and `random_state_sampling` is set, will sample a
normal distribution tensor of the above shape. Otherwise, None will be passed to the RNN.
add_sos: bool flag, whether a zero vector describing a "start of signal" token should be
prepended to the above "y" tensor. When set, output size is (B, U + 1, H).
batch_size: An optional int, specifying the batch size of the `y` tensor.
Can be infered if `y` and `state` is None. But if both are None, then batch_size cannot be None.
Returns:
A tuple (g, hid) such that -
If add_sos is False:
g: (B, U, H)
hid: (h, c) where h is the final sequence hidden state and c is the final cell state:
h (tensor), shape (L, B, H)
c (tensor), shape (L, B, H)
If add_sos is True:
g: (B, U + 1, H)
hid: (h, c) where h is the final sequence hidden state and c is the final cell state:
h (tensor), shape (L, B, H)
c (tensor), shape (L, B, H)
"""
raise NotImplementedError()
@abstractmethod
def initialize_state(self, y: torch.Tensor) -> List[torch.Tensor]:
"""
Initialize the state of the RNN layers, with same dtype and device as input `y`.
Args:
y: A torch.Tensor whose device the generated states will be placed on.
Returns:
List of torch.Tensor, each of shape [L, B, H], where
L = Number of RNN layers
B = Batch size
H = Hidden size of RNN.
"""
raise NotImplementedError()
@abstractmethod
def score_hypothesis(
self, hypothesis: Hypothesis, cache: Dict[Tuple[int], Any]
) -> (torch.Tensor, List[torch.Tensor], torch.Tensor):
"""
Similar to the predict() method, instead this method scores a Hypothesis during beam search.
Hypothesis is a dataclass representing one hypothesis in a Beam Search.
Args:
hypothesis: Refer to rnnt_utils.Hypothesis.
cache: Dict which contains a cache to avoid duplicate computations.
Returns:
Returns a tuple (y, states, lm_token) such that:
y is a torch.Tensor of shape [1, 1, H] representing the score of the last token in the Hypothesis.
state is a list of RNN states, each of shape [L, 1, H].
lm_token is the final integer token of the hypothesis.
"""
raise NotImplementedError()
def batch_score_hypothesis(
self, hypotheses: List[Hypothesis], cache: Dict[Tuple[int], Any], batch_states: List[torch.Tensor]
) -> (torch.Tensor, List[torch.Tensor], torch.Tensor):
"""
Used for batched beam search algorithms. Similar to score_hypothesis method.
Args:
hypothesis: List of Hypotheses. Refer to rnnt_utils.Hypothesis.
cache: Dict which contains a cache to avoid duplicate computations.
batch_states: List of torch.Tensor which represent the states of the RNN for this batch.
Each state is of shape [L, B, H]
Returns:
Returns a tuple (b_y, b_states, lm_tokens) such that:
b_y is a torch.Tensor of shape [B, 1, H] representing the scores of the last tokens in the Hypotheses.
b_state is a list of list of RNN states, each of shape [L, B, H].
Represented as B x List[states].
lm_token is a list of the final integer tokens of the hypotheses in the batch.
"""
raise NotImplementedError()
def batch_initialize_states(self, batch_states: List[torch.Tensor], decoder_states: List[List[torch.Tensor]]):
"""
Create batch of decoder states.
Args:
batch_states (list): batch of decoder states
([L x (B, H)], [L x (B, H)])
decoder_states (list of list): list of decoder states
[B x ([L x (1, H)], [L x (1, H)])]
Returns:
batch_states (tuple): batch of decoder states
([L x (B, H)], [L x (B, H)])
"""
raise NotImplementedError()
def batch_select_state(self, batch_states: List[torch.Tensor], idx: int) -> List[List[torch.Tensor]]:
"""Get decoder state from batch of states, for given id.
Args:
batch_states (list): batch of decoder states
([L x (B, H)], [L x (B, H)])
idx (int): index to extract state from batch of states
Returns:
(tuple): decoder states for given id
([L x (1, H)], [L x (1, H)])
"""
raise NotImplementedError()
@@ -0,0 +1,306 @@
# Copyright (c) 2020, 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.
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from typing import Optional, Tuple
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from nemo.collections.asr.models.wav2vec.wav2vec_config import Wav2VecMaskType
from nemo.core import NeuralModule
from nemo.core.neural_types import EncodedRepresentation, LossType, NeuralType
class GumbelVectorQuantizer(NeuralModule):
def __init__(
self,
dim,
num_vars,
temp,
groups,
combine_groups,
vq_dim,
time_first,
activation=nn.GELU(),
weight_proj_depth=1,
weight_proj_factor=1,
):
"""Vector quantization using gumbel softmax
Args:
dim: input dimension (channels)
num_vars: number of quantized vectors per group
temp: temperature for training. this should be a tuple of 3 elements: (start, stop, decay factor)
groups: number of groups for vector quantization
combine_groups: whether to use the vectors for all groups
vq_dim: dimensionality of the resulting quantized vector
time_first: if true, expect input in BxTxC format, otherwise in BxCxT
activation: what activation to use (should be a module). this is only used if weight_proj_depth is > 1
weight_proj_depth: number of layers (with activation in between) to project input before computing logits
weight_proj_factor: this is used only if weight_proj_depth is > 1. scales the inner dimensionality of
projections by this factor
"""
super().__init__()
self.groups = groups
self.combine_groups = combine_groups
self.input_dim = dim
self.num_vars = num_vars
self.time_first = time_first
assert vq_dim % groups == 0, f"dim {vq_dim} must be divisible by groups {groups} for concatenation"
var_dim = vq_dim // groups
num_groups = groups if not combine_groups else 1
self.vars = nn.Parameter(torch.FloatTensor(1, num_groups * num_vars, var_dim))
nn.init.uniform_(self.vars)
if weight_proj_depth > 1:
def block(input_dim, output_dim):
return nn.Sequential(nn.Linear(input_dim, output_dim), activation)
inner_dim = self.input_dim * weight_proj_factor
self.weight_proj = nn.Sequential(
*[block(self.input_dim if i == 0 else inner_dim, inner_dim) for i in range(weight_proj_depth - 1)],
nn.Linear(inner_dim, groups * num_vars),
)
else:
self.weight_proj = nn.Linear(self.input_dim, groups * num_vars)
nn.init.normal_(self.weight_proj.weight, mean=0, std=1)
nn.init.zeros_(self.weight_proj.bias)
assert len(temp) == 3, "Quantize temperature should be a tuple of 3 elements: (start, stop, decay factor)"
self.max_temp, self.min_temp, self.temp_decay = temp
self.curr_temp = self.max_temp
self.codebook_indices = None
def set_num_updates(self, num_updates):
self.curr_temp = max(self.max_temp * self.temp_decay ** num_updates, self.min_temp)
def get_codebook_indices(self):
if self.codebook_indices is None:
from itertools import product
p = [range(self.num_vars)] * self.groups
inds = list(product(*p))
self.codebook_indices = torch.tensor(inds, dtype=torch.long, device=self.vars.device).flatten()
if not self.combine_groups:
self.codebook_indices = self.codebook_indices.view(self.num_vars ** self.groups, -1)
for b in range(1, self.groups):
self.codebook_indices[:, b] += self.num_vars * b
self.codebook_indices = self.codebook_indices.flatten()
return self.codebook_indices
def sample_from_codebook(self, b, n):
indices = self.get_codebook_indices()
indices = indices.view(-1, self.groups)
cb_size = indices.size(0)
assert n < cb_size, f"sample size {n} is greater than size of codebook {cb_size}"
sample_idx = torch.randint(low=0, high=cb_size, size=(b * n,))
indices = indices[sample_idx]
z = self.vars.squeeze(0).index_select(0, indices.flatten()).view(b, n, -1)
return z
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
if self.time_first:
return {"x": NeuralType(('B', 'T', 'D'), EncodedRepresentation())}
return {"x": NeuralType(('B', 'D', 'T'), EncodedRepresentation())}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
if self.time_first:
return {
"x": NeuralType(('B', 'T', 'D'), EncodedRepresentation()),
"quantize_prob_ppl": NeuralType(elements_type=LossType()),
}
return {
"x": NeuralType(('B', 'D', 'T'), EncodedRepresentation()),
"quantize_prob_ppl": NeuralType(elements_type=LossType()),
}
def forward(self, x):
if not self.time_first:
x = x.transpose(1, 2)
bsz, tsz, fsz = x.shape
x = x.reshape(-1, fsz)
x = self.weight_proj(x)
x = x.view(bsz * tsz * self.groups, -1)
_, k = x.max(-1)
hard_x = x.new_zeros(*x.shape).scatter_(-1, k.view(-1, 1), 1.0).view(bsz * tsz, self.groups, -1)
# Calculate quantize prob perplexity
num_vars = self.num_vars * self.groups
avg_probs = torch.softmax(x.view(bsz * tsz, self.groups, -1).float(), dim=-1).mean(dim=0)
quantize_prob_ppl = torch.exp(-torch.sum(avg_probs * torch.log(avg_probs + 1e-7), dim=-1)).sum()
quantize_prob_ppl = (num_vars - quantize_prob_ppl) / num_vars
if self.training:
x = F.gumbel_softmax(x.float(), tau=self.curr_temp, hard=True).type_as(x)
else:
x = hard_x
x = x.view(bsz * tsz, -1)
vars = self.vars
if self.combine_groups:
vars = vars.repeat(1, self.groups, 1)
x = x.unsqueeze(-1) * vars
x = x.view(bsz * tsz, self.groups, self.num_vars, -1)
x = x.sum(-2)
x = x.view(bsz, tsz, -1)
cur_codebook_temp = self.curr_temp
if not self.time_first:
x = x.transpose(1, 2) # BTC -> BCT
return x, quantize_prob_ppl, cur_codebook_temp
def compute_mask_indices(
shape: Tuple[int, int],
padding_mask: Optional[torch.Tensor],
mask_prob: float,
mask_length: int,
mask_type: Wav2VecMaskType = Wav2VecMaskType.static,
mask_other: float = 0.0,
min_masks: int = 0,
no_overlap: bool = False,
min_space: int = 0,
) -> np.ndarray:
"""
Computes random mask spans for a given shape
Args:
shape: the the shape for which to compute masks.
should be of size 2 where first element is batch size and 2nd is timesteps
padding_mask: optional padding mask of the same size as shape, which will prevent masking padded elements
mask_prob: probability for each token to be chosen as start of the span to be masked. this will be multiplied by
number of timesteps divided by length of mask span to mask approximately this percentage of all elements.
however due to overlaps, the actual number will be smaller (unless no_overlap is True)
mask_type: how to compute mask lengths
static = fixed size
uniform = sample from uniform distribution [mask_other, mask_length*2]
normal = sample from normal distribution with mean mask_length and stdev mask_other. mask is min 1 element
poisson = sample from possion distribution with lambda = mask length
min_masks: minimum number of masked spans
no_overlap: if false, will switch to an alternative recursive algorithm that prevents spans from overlapping
min_space: only used if no_overlap is True, this is how many elements to keep unmasked between spans
"""
bsz, all_sz = shape
mask = np.full((bsz, all_sz), False)
all_num_mask = int(
# add a random number for probabilistic rounding
mask_prob * all_sz / float(mask_length)
+ np.random.rand()
)
all_num_mask = max(min_masks, all_num_mask)
mask_idcs = []
for i in range(bsz):
if padding_mask is not None:
sz = all_sz - padding_mask[i].long().sum().item()
num_mask = int(
# add a random number for probabilistic rounding
mask_prob * sz / float(mask_length)
+ np.random.rand()
)
num_mask = max(min_masks, num_mask)
else:
sz = all_sz
num_mask = all_num_mask
if mask_type.value is Wav2VecMaskType.static.value:
lengths = np.full(num_mask, mask_length)
elif mask_type.value is Wav2VecMaskType.uniform:
lengths = np.random.randint(mask_other, mask_length * 2 + 1, size=num_mask)
elif mask_type.value is Wav2VecMaskType.normal.value:
lengths = np.random.normal(mask_length, mask_other, size=num_mask)
lengths = [max(1, int(round(x))) for x in lengths]
elif mask_type.value is Wav2VecMaskType.poisson.value:
lengths = np.random.poisson(mask_length, size=num_mask)
lengths = [int(round(x)) for x in lengths]
else:
raise Exception("unknown mask selection " + str(mask_type))
if sum(lengths) == 0:
lengths[0] = min(mask_length, sz - 1)
if no_overlap:
mask_idc = []
def arrange(s, e, length, keep_length):
span_start = np.random.randint(s, e - length)
mask_idc.extend(span_start + i for i in range(length))
new_parts = []
if span_start - s - min_space >= keep_length:
new_parts.append((s, span_start - min_space + 1))
if e - span_start - keep_length - min_space > keep_length:
new_parts.append((span_start + length + min_space, e))
return new_parts
parts = [(0, sz)]
min_length = min(lengths)
for length in sorted(lengths, reverse=True):
lens = np.fromiter((e - s if e - s >= length + min_space else 0 for s, e in parts), np.int)
l_sum = np.sum(lens)
if l_sum == 0:
break
probs = lens / np.sum(lens)
c = np.random.choice(len(parts), p=probs)
s, e = parts.pop(c)
parts.extend(arrange(s, e, length, min_length))
mask_idc = np.asarray(mask_idc)
else:
min_len = min(lengths)
if sz - min_len <= num_mask:
min_len = sz - num_mask - 1
mask_idc = np.random.choice(sz - min_len, num_mask, replace=False)
mask_idc = np.asarray([mask_idc[j] + offset for j in range(len(mask_idc)) for offset in range(lengths[j])])
mask_idcs.append(np.unique(mask_idc[mask_idc < sz]))
min_len = min([len(m) for m in mask_idcs])
for i, mask_idc in enumerate(mask_idcs):
if len(mask_idc) > min_len:
mask_idc = np.random.choice(mask_idc, min_len, replace=False)
mask[i, mask_idc] = True
return mask
@@ -0,0 +1,13 @@
# Copyright (c) 2020, 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.
@@ -0,0 +1,39 @@
# Copyright (c) 2020, 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.
#
# Copyright (c) 2018 Ryan Leary
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This file contains code artifacts adapted from https://github.com/ryanleary/patter
"""
ALIAS FILE for backward compatibility
"""
from nemo.collections.asr.parts.preprocessing.features import *
@@ -0,0 +1,15 @@
# Copyright (c) 2021, 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.
from nemo.collections.asr.parts.mixins.mixins import ASRBPEMixin, ASRModuleMixin, DiarizationMixin
@@ -0,0 +1,193 @@
# Copyright (c) 2020, 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 abc import ABC, abstractmethod
from typing import List
from omegaconf import DictConfig, OmegaConf, open_dict
from nemo.collections.asr.parts.utils import asr_module_utils
from nemo.collections.common import tokenizers
from nemo.utils import logging
class ASRBPEMixin(ABC):
""" ASR BPE Mixin class that sets up a Tokenizer via a config
This mixin class adds the method `_setup_tokenizer(...)`, which can be used by ASR models
which depend on subword tokenization.
The setup_tokenizer method adds the following parameters to the class -
- tokenizer_cfg: The resolved config supplied to the tokenizer (with `dir` and `type` arguments).
- tokenizer_dir: The directory path to the tokenizer vocabulary + additional metadata.
- tokenizer_type: The type of the tokenizer. Currently supports `bpe` and `wpe`.
- vocab_path: Resolved path to the vocabulary text file.
In addition to these variables, the method will also instantiate and preserve a tokenizer
(subclass of TokenizerSpec) if successful, and assign it to self.tokenizer.
"""
def _setup_tokenizer(self, tokenizer_cfg: DictConfig):
# Prevent tokenizer parallelism (unless user has explicitly set it)
if 'TOKENIZERS_PARALLELISM' not in os.environ:
os.environ['TOKENIZERS_PARALLELISM'] = 'false'
self.tokenizer_cfg = OmegaConf.to_container(tokenizer_cfg, resolve=True) # type: dict
self.tokenizer_dir = self.tokenizer_cfg.pop('dir') # Remove tokenizer directory
self.tokenizer_type = self.tokenizer_cfg.pop('type').lower() # Remove tokenizer_type
self.hf_tokenizer_kwargs = self.tokenizer_cfg.pop("hf_kwargs", {}) # Remove HF tokenizer kwargs
# Preserve config
if hasattr(self, 'cfg') and 'tokenizer' in self.cfg:
self.cfg.tokenizer.dir = self.tokenizer_dir
self.cfg.tokenizer.type = self.tokenizer_type
if 'hf_kwargs' in tokenizer_cfg:
with open_dict(self.cfg.tokenizer):
self.cfg.tokenizer.hf_kwargs = tokenizer_cfg.get('hf_kwargs')
if self.tokenizer_type not in ['bpe', 'wpe']:
raise ValueError(
"`tokenizer.type` must be either `bpe` for SentencePiece tokenizer or "
"`wpe` for BERT based tokenizer"
)
if self.tokenizer_type == 'bpe':
# This is a BPE Tokenizer
if 'model_path' in self.tokenizer_cfg:
model_path = self.tokenizer_cfg.get('model_path')
else:
model_path = os.path.join(self.tokenizer_dir, 'tokenizer.model')
model_path = self.register_artifact('tokenizer.model_path', model_path)
self.model_path = model_path
if 'special_tokens' in self.tokenizer_cfg:
special_tokens = self.tokenizer_cfg['special_tokens']
else:
special_tokens = None
# Update special tokens
self.tokenizer = tokenizers.SentencePieceTokenizer(
model_path=model_path, special_tokens=special_tokens, legacy=True
)
if 'vocab_path' in self.tokenizer_cfg:
vocab_path = self.tokenizer_cfg.get('vocab_path')
else:
vocab_path = os.path.join(self.tokenizer_dir, 'vocab.txt')
vocab_path = self.register_artifact('tokenizer.vocab_path', vocab_path)
self.vocab_path = vocab_path
try:
if 'spe_tokenizer_vocab' in self.tokenizer_cfg:
spe_vocab_path = self.tokenizer_cfg.get('spe_tokenizer_vocab')
else:
spe_vocab_path = os.path.join(self.tokenizer_dir, 'tokenizer.vocab')
spe_vocab_path = self.register_artifact('tokenizer.spe_tokenizer_vocab', spe_vocab_path)
self.spe_vocab_path = spe_vocab_path
except FileNotFoundError:
# fallback case for older checkpoints that did not preserve the tokenizer.vocab
self.spe_vocab_path = None
vocabulary = {'<unk>': 0}
with open(vocab_path) as f:
for i, piece in enumerate(f):
piece = piece.replace('\n', '')
vocabulary[piece] = i + 1
# wrapper method to get vocabulary conveniently
def get_vocab():
return vocabulary
# attach utility values to the tokenizer wrapper
self.tokenizer.tokenizer.vocab_size = len(vocabulary)
self.tokenizer.tokenizer.get_vocab = get_vocab
self.tokenizer.tokenizer.all_special_tokens = self.tokenizer.special_token_to_id
else:
# This is a WPE Tokenizer
# If path from previous registration exists, remove it
if 'vocab_path' in self.tokenizer_cfg:
vocab_path = self.tokenizer_cfg.get('vocab_path')
else:
vocab_path = os.path.join(self.tokenizer_dir, 'vocab.txt')
vocab_path = self.register_artifact('tokenizer.vocab_path', vocab_path)
self.vocab_path = vocab_path
# If path from previous registration exists, remove it
if 'vocab_path' in self.tokenizer_cfg:
self.tokenizer_cfg.pop('vocab_path')
self.tokenizer = tokenizers.AutoTokenizer(
pretrained_model_name='bert-base-cased',
vocab_file=self.vocab_path,
mask_token=self.hf_tokenizer_kwargs.get('mask_token', None),
bos_token=self.hf_tokenizer_kwargs.get('bos_token', None),
eos_token=self.hf_tokenizer_kwargs.get('eos_token', None),
pad_token=self.hf_tokenizer_kwargs.get('pad_token', None),
sep_token=self.hf_tokenizer_kwargs.get('sep_token', None),
cls_token=self.hf_tokenizer_kwargs.get('cls_token', None),
unk_token=self.hf_tokenizer_kwargs.get('unk_token', None),
use_fast=self.hf_tokenizer_kwargs.get('use_fast', False),
)
logging.info(
"Tokenizer {} initialized with {} tokens".format(
self.tokenizer.__class__.__name__, self.tokenizer.vocab_size
)
)
class ASRModuleMixin(ABC):
"""
ASRModuleMixin is a mixin class added to ASR models in order to add methods that are specific
to a particular instantiation of a module inside of an ASRModel.
Each method should first check that the module is present within the subclass, and support additional
functionality if the corresponding module is present.
"""
def change_conv_asr_se_context_window(self, context_window: int, update_config: bool = True):
"""
Update the context window of the SqueezeExcitation module if the provided model contains an
`encoder` which is an instance of `ConvASREncoder`.
Args:
context_window: An integer representing the number of input timeframes that will be used
to compute the context. Each timeframe corresponds to a single window stride of the
STFT features.
Say the window_stride = 0.01s, then a context window of 128 represents 128 * 0.01 s
of context to compute the Squeeze step.
update_config: Whether to update the config or not with the new context window.
"""
asr_module_utils.change_conv_asr_se_context_window(
self, context_window=context_window, update_config=update_config
)
class DiarizationMixin(ABC):
@abstractmethod
def diarize(self, paths2audio_files: List[str], batch_size: int = 1) -> List[str]:
"""
Takes paths to audio files and returns speaker labels
Args:
paths2audio_files: paths to audio fragment to be transcribed
Returns:
Speaker labels
"""
pass
@@ -0,0 +1,15 @@
# Copyright (c) 2021, 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.
from nemo.collections.asr.parts.numba.rnnt_loss.rnnt_pytorch import RNNTLossNumba
@@ -0,0 +1,16 @@
# Copyright (c) 2021, 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.
from nemo.collections.asr.parts.numba.rnnt_loss.rnnt import rnnt_loss_cpu, rnnt_loss_gpu
from nemo.collections.asr.parts.numba.rnnt_loss.rnnt_pytorch import RNNTLossNumba
@@ -0,0 +1,225 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 multiprocessing
import torch
from numba import cuda
from nemo.collections.asr.parts.numba.rnnt_loss.utils import global_constants, rnnt_helper
from nemo.collections.asr.parts.numba.rnnt_loss.utils.cpu_utils import cpu_rnnt
from nemo.collections.asr.parts.numba.rnnt_loss.utils.cuda_utils import gpu_rnnt
def rnnt_loss_cpu(
acts: torch.Tensor,
labels: torch.Tensor,
input_lengths: torch.Tensor,
label_lengths: torch.Tensor,
costs: torch.Tensor,
grads: torch.Tensor,
blank_label: int,
num_threads: int,
):
"""
Wrapper method for accessing CPU RNNT loss.
CPU implementation ported from [HawkAaron/warp-transducer](https://github.com/HawkAaron/warp-transducer).
Args:
acts: Activation tensor of shape [B, T, U, V+1].
labels: Ground truth labels of shape [B, U].
input_lengths: Lengths of the acoustic sequence as a vector of ints [B].
label_lengths: Lengths of the target sequence as a vector of ints [B].
costs: Zero vector of length [B] in which costs will be set.
grads: Zero tensor of shape [B, T, U, V+1] where the gradient will be set.
blank_label: Index of the blank token in the vocabulary.
num_threads: Number of threads for OpenMP.
"""
# aliases
log_probs = acts
flat_labels = labels
minibatch_size = log_probs.shape[0]
maxT = log_probs.shape[1]
maxU = log_probs.shape[2]
alphabet_size = log_probs.shape[3]
if num_threads < 0:
num_threads = multiprocessing.cpu_count()
num_threads = max(1, num_threads) # have to use at least 1 thread
gpu_size, status = rnnt_helper.get_workspace_size(maxT, maxU, minibatch_size, gpu=False)
if status != global_constants.RNNTStatus.RNNT_STATUS_SUCCESS:
raise RuntimeError("Invalid parameter passed when calculating working space memory")
cpu_workspace = torch.zeros(gpu_size, device=log_probs.device, dtype=log_probs.dtype, requires_grad=False)
### VIEW TENSORS AS VECTORS FOR POINTER INDEXING ###
log_probs, acts_shape = rnnt_helper.flatten_tensor(log_probs)
flat_labels, labels_shape = rnnt_helper.flatten_tensor(flat_labels)
wrapper = cpu_rnnt.CPURNNT(
minibatch=minibatch_size,
maxT=maxT,
maxU=maxU,
alphabet_size=alphabet_size,
workspace=cpu_workspace,
blank=blank_label,
num_threads=num_threads,
batch_first=True,
)
if grads is None:
status = wrapper.score_forward(
log_probs=log_probs.data,
costs=costs,
flat_labels=flat_labels.data,
label_lengths=label_lengths.data,
input_lengths=input_lengths.data,
)
if status != global_constants.RNNTStatus.RNNT_STATUS_SUCCESS:
raise RuntimeError("Could not calculate forward scores")
else:
### FLATTEN GRAD TENSOR ###
grads, grads_shape = rnnt_helper.flatten_tensor(grads)
status = wrapper.cost_and_grad(
log_probs=log_probs.data,
grads=grads.data,
costs=costs,
flat_labels=flat_labels.data,
label_lengths=label_lengths.data,
input_lengths=input_lengths.data,
)
if status != global_constants.RNNTStatus.RNNT_STATUS_SUCCESS:
raise RuntimeError("Could not calculate forward scores")
del cpu_workspace, wrapper
return True
def rnnt_loss_gpu(
acts: torch.Tensor,
labels: torch.Tensor,
input_lengths: torch.Tensor,
label_lengths: torch.Tensor,
costs: torch.Tensor,
grads: torch.Tensor,
blank_label: int,
num_threads: int,
):
"""
Wrapper method for accessing GPU RNNT loss.
CUDA implementation ported from [HawkAaron/warp-transducer](https://github.com/HawkAaron/warp-transducer).
Args:
acts: Activation tensor of shape [B, T, U, V+1].
labels: Ground truth labels of shape [B, U].
input_lengths: Lengths of the acoustic sequence as a vector of ints [B].
label_lengths: Lengths of the target sequence as a vector of ints [B].
costs: Zero vector of length [B] in which costs will be set.
grads: Zero tensor of shape [B, T, U, V+1] where the gradient will be set.
blank_label: Index of the blank token in the vocabulary.
num_threads: Number of threads for OpenMP.
"""
minibatch_size = acts.shape[0]
maxT = acts.shape[1]
maxU = acts.shape[2]
alphabet_size = acts.shape[3]
if hasattr(cuda, 'external_stream'):
stream = cuda.external_stream(torch.cuda.current_stream(acts.device).cuda_stream)
else:
stream = cuda.default_stream()
if num_threads < 0:
num_threads = multiprocessing.cpu_count()
num_threads = max(1, num_threads) # have to use at least 1 thread
gpu_size, status = rnnt_helper.get_workspace_size(maxT, maxU, minibatch_size, gpu=True)
if status != global_constants.RNNTStatus.RNNT_STATUS_SUCCESS:
raise RuntimeError("Invalid parameter passed when calculating working space memory")
# Select GPU index
cuda.select_device(acts.device.index)
gpu_workspace = torch.zeros(gpu_size, device=acts.device, dtype=acts.dtype, requires_grad=False)
### VIEW TENSORS AS VECTORS FOR POINTER INDEXING ###
acts, acts_shape = rnnt_helper.flatten_tensor(acts)
### REPRESENT THE CUDA ARRAY INTERFACE OF COSTS VECTOR ###
costs_repr = cuda.as_cuda_array(costs, sync=False) # NO COPY OF DATA, JUST CHANGE REPRESENTATION
wrapper = gpu_rnnt.GPURNNT(
minibatch=minibatch_size,
maxT=maxT,
maxU=maxU,
alphabet_size=alphabet_size,
workspace=gpu_workspace,
blank=blank_label,
num_threads=num_threads,
stream=stream,
)
if grads is None:
status = wrapper.score_forward(
acts=acts.data,
costs=costs_repr,
pad_labels=labels.data,
label_lengths=label_lengths.data,
input_lengths=input_lengths.data,
)
if status != global_constants.RNNTStatus.RNNT_STATUS_SUCCESS:
raise RuntimeError("Could not calculate forward scores")
else:
### FLATTEN GRAD TENSOR ###
grads, grads_shape = rnnt_helper.flatten_tensor(grads)
status = wrapper.cost_and_grad(
acts=acts.data,
grads=grads.data,
costs=costs_repr,
pad_labels=labels.data,
label_lengths=label_lengths.data,
input_lengths=input_lengths.data,
)
if status != global_constants.RNNTStatus.RNNT_STATUS_SUCCESS:
raise RuntimeError("Could not calculate forward scores")
del gpu_workspace, wrapper
return True
@@ -0,0 +1,213 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 numpy as np
import torch
from torch.autograd import Function, Variable
from torch.nn import Module
def _assert_no_grad(tensor):
assert not tensor.requires_grad, (
"gradients only computed for log_probs - please " "mark other tensors as not requiring gradients"
)
def forward_pass(log_probs, labels, blank):
"""
Computes probability of the forward variable alpha.
Args:
log_probs: Tensor of shape [T, U, V+1]
labels: Labels of shape [B, U]
blank: Index of the blank token.
Returns:
A tuple of the forward variable probabilities - alpha of shape [T, U]
and the log likelihood of this forward step.
"""
T, U, _ = log_probs.shape
alphas = np.zeros((T, U), dtype='f')
for t in range(1, T):
alphas[t, 0] = alphas[t - 1, 0] + log_probs[t - 1, 0, blank]
for u in range(1, U):
alphas[0, u] = alphas[0, u - 1] + log_probs[0, u - 1, labels[u - 1]]
for t in range(1, T):
for u in range(1, U):
no_emit = alphas[t - 1, u] + log_probs[t - 1, u, blank]
emit = alphas[t, u - 1] + log_probs[t, u - 1, labels[u - 1]]
alphas[t, u] = np.logaddexp(emit, no_emit)
loglike = alphas[T - 1, U - 1] + log_probs[T - 1, U - 1, blank]
return alphas, loglike
def backward_pass(log_probs, labels, blank):
"""
Computes probability of the backward variable beta.
Args:
log_probs: Tensor of shape [T, U, V+1]
labels: Labels of shape [B, U]
blank: Index of the blank token.
Returns:
A tuple of the backward variable probabilities - beta of shape [T, U]
and the log likelihood of this backward step.
"""
T, U, _ = log_probs.shape
betas = np.zeros((T, U), dtype='f')
betas[T - 1, U - 1] = log_probs[T - 1, U - 1, blank]
for t in reversed(range(T - 1)):
betas[t, U - 1] = betas[t + 1, U - 1] + log_probs[t, U - 1, blank]
for u in reversed(range(U - 1)):
betas[T - 1, u] = betas[T - 1, u + 1] + log_probs[T - 1, u, labels[u]]
for t in reversed(range(T - 1)):
for u in reversed(range(U - 1)):
no_emit = betas[t + 1, u] + log_probs[t, u, blank]
emit = betas[t, u + 1] + log_probs[t, u, labels[u]]
betas[t, u] = np.logaddexp(emit, no_emit)
return betas, betas[0, 0]
def compute_gradient(log_probs, alphas, betas, labels, blank):
"""
Computes the gradients of the log_probs with respect to the log probability of this step occuring.
Args:
Args:
log_probs: Tensor of shape [T, U, V+1]
alphas: Tensor of shape [T, U] which represents the forward variable.
betas: Tensor of shape [T, U] which represents the backward variable.
labels: Labels of shape [B, U]
blank: Index of the blank token.
Returns:
Gradients of shape [T, U, V+1] with respect to the forward log probability
"""
T, U, _ = log_probs.shape
grads = np.full(log_probs.shape, -float("inf"))
log_like = betas[0, 0]
# // grad to last blank transition
grads[T - 1, U - 1, blank] = alphas[T - 1, U - 1]
grads[: T - 1, :, blank] = alphas[: T - 1, :] + betas[1:, :]
for u, l in enumerate(labels):
grads[:, u, l] = alphas[:, u] + betas[:, u + 1]
grads = -np.exp(grads + log_probs - log_like)
return grads
def transduce(log_probs, labels, blank=0):
"""
Args:
log_probs: 3D array with shape
[input len, output len + 1, vocab size]
labels: 1D array with shape [output time steps]
Returns:
float: The negative log-likelihood
3D array: Gradients with respect to the
unnormalized input actications
"""
alphas, ll_forward = forward_pass(log_probs, labels, blank)
betas, ll_backward = backward_pass(log_probs, labels, blank)
grads = compute_gradient(log_probs, alphas, betas, labels, blank)
return -ll_forward, grads
def transduce_batch(log_probs, labels, flen, glen, blank=0):
"""
Compute the transducer loss of the batch.
Args:
log_probs: [B, T, U, V+1]. Activation matrix normalized with log-softmax.
labels: [B, U+1] - ground truth labels with <SOS> padded as blank token in the beginning.
flen: Length vector of the acoustic sequence.
glen: Length vector of the target sequence.
blank: Id of the blank token.
Returns:
Batch of transducer forward log probabilities (loss) and the gradients of the activation matrix.
"""
grads = np.zeros_like(log_probs)
costs = []
for b in range(log_probs.shape[0]):
t = int(flen[b])
u = int(glen[b]) + 1
ll, g = transduce(log_probs[b, :t, :u, :], labels[b, : u - 1], blank)
grads[b, :t, :u, :] = g
costs.append(ll)
return costs, grads
class _RNNT(Function):
@staticmethod
def forward(ctx, acts, labels, act_lens, label_lens, blank):
costs, grads = transduce_batch(
acts.detach().cpu().numpy(), labels.cpu().numpy(), act_lens.cpu().numpy(), label_lens.cpu().numpy(), blank,
)
costs = torch.FloatTensor([sum(costs)])
grads = torch.Tensor(grads).to(acts)
ctx.grads = Variable(grads)
return costs
@staticmethod
def backward(ctx, grad_output):
return ctx.grads, None, None, None, None
class RNNTLoss(Module):
"""
Parameters:
`blank_label` (int): default 0 - label index of blank token
"""
def __init__(self, blank: int = 0):
super(RNNTLoss, self).__init__()
self.blank = blank
self.rnnt = _RNNT.apply
def forward(self, acts, labels, act_lens, label_lens):
assert len(labels.size()) == 2
_assert_no_grad(labels)
_assert_no_grad(act_lens)
_assert_no_grad(label_lens)
acts = torch.nn.functional.log_softmax(acts, -1)
return self.rnnt(acts, labels, act_lens, label_lens, self.blank)
@@ -0,0 +1,186 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 torch
from torch.autograd import Function
from torch.nn import Module
from nemo.collections.asr.parts.numba.rnnt_loss import rnnt
__all__ = ['rnnt_loss', 'RNNTLossNumba']
class _RNNTNumba(Function):
@staticmethod
def forward(ctx, acts, labels, act_lens, label_lens, blank, reduction):
"""
log_probs: Tensor of (batch x seqLength x labelLength x outputDim) containing output from network
labels: 2 dimensional Tensor containing all the targets of the batch with zero padded
act_lens: Tensor of size (batch) containing size of each output sequence from the network
label_lens: Tensor of (batch) containing label length of each example
"""
is_cuda = acts.is_cuda
certify_inputs(acts, labels, act_lens, label_lens)
loss_func = rnnt.rnnt_loss_gpu if is_cuda else rnnt.rnnt_loss_cpu
grads = torch.zeros_like(acts) if acts.requires_grad else None
minibatch_size = acts.size(0)
costs = torch.zeros(minibatch_size, device=acts.device, dtype=acts.dtype)
loss_func(
acts,
labels=labels,
input_lengths=act_lens,
label_lengths=label_lens,
costs=costs,
grads=grads,
blank_label=blank,
num_threads=0,
)
if reduction in ['sum', 'mean']:
costs = costs.sum().unsqueeze_(-1)
if reduction == 'mean':
costs /= minibatch_size
if grads is not None:
grads /= minibatch_size
# costs = costs.to(log_probs.device)
ctx.grads = grads
return costs
@staticmethod
def backward(ctx, grad_output):
if grad_output is not None and ctx.grads is not None:
grad_output = grad_output.view(-1, 1, 1, 1).to(ctx.grads)
return ctx.grads.mul_(grad_output), None, None, None, None, None
def rnnt_loss(acts, labels, act_lens, label_lens, blank=0, reduction='mean'):
"""RNN Transducer Loss
Args:
acts: Tensor of (batch x seqLength x labelLength x outputDim) containing output from network
labels: 2 dimensional Tensor containing all the targets of the batch with zero padded
act_lens: Tensor of size (batch) containing size of each output sequence from the network
label_lens: Tensor of (batch) containing label length of each example
blank (int, optional): blank label. Default: 0.
reduction (string, optional): Specifies the reduction to apply to the output:
'none' | 'mean' | 'sum'. 'none': no reduction will be applied,
'mean': the output losses will be divided by the target lengths and
then the mean over the batch is taken. Default: 'mean'
"""
if not acts.is_cuda:
acts = torch.nn.functional.log_softmax(acts, -1)
return _RNNTNumba.apply(acts, labels, act_lens, label_lens, blank, reduction)
class RNNTLossNumba(Module):
"""
Parameters:
blank (int, optional): blank label. Default: 0.
reduction (string, optional): Specifies the reduction to apply to the output:
'none' | 'mean' | 'sum'. 'none': no reduction will be applied,
'mean': the output losses will be divided by the target lengths and
then the mean over the batch is taken. Default: 'mean'
"""
def __init__(self, blank=0, reduction='mean'):
super(RNNTLossNumba, self).__init__()
self.blank = blank
self.reduction = reduction
self.loss = _RNNTNumba.apply
def forward(self, acts, labels, act_lens, label_lens):
"""
log_probs: Tensor of (batch x seqLength x labelLength x outputDim) containing output from network
labels: 2 dimensional Tensor containing all the targets of the batch with zero padded
act_lens: Tensor of size (batch) containing size of each output sequence from the network
label_lens: Tensor of (batch) containing label length of each example
"""
if not acts.is_cuda:
# NOTE manually done log_softmax for CPU version,
# log_softmax is computed within GPU version.
acts = torch.nn.functional.log_softmax(acts, -1)
return self.loss(acts, labels, act_lens, label_lens, self.blank, self.reduction)
def check_type(var, t, name):
if var.dtype is not t:
raise TypeError("{} must be {}".format(name, t))
def check_contiguous(var, name):
if not var.is_contiguous():
raise ValueError("{} must be contiguous".format(name))
def check_dim(var, dim, name):
if len(var.shape) != dim:
raise ValueError("{} must be {}D".format(name, dim))
def certify_inputs(log_probs, labels, lengths, label_lengths):
# check_type(log_probs, torch.float32, "log_probs")
check_type(labels, torch.int32, "labels")
check_type(label_lengths, torch.int32, "label_lengths")
check_type(lengths, torch.int32, "lengths")
check_contiguous(log_probs, "log_probs")
check_contiguous(labels, "labels")
check_contiguous(label_lengths, "label_lengths")
check_contiguous(lengths, "lengths")
if lengths.shape[0] != log_probs.shape[0]:
raise ValueError(
f"Must have a length per example. "
f"Given lengths dim: {lengths.shape[0]}, "
f"Log probs dim : {log_probs.shape[0]}"
)
if label_lengths.shape[0] != log_probs.shape[0]:
raise ValueError(
"Must have a label length per example. "
f"Given label lengths dim : {label_lengths.shape[0]}, "
f"Log probs dim : {log_probs.shape[0]}"
)
check_dim(log_probs, 4, "log_probs")
check_dim(labels, 2, "labels")
check_dim(lengths, 1, "lenghts")
check_dim(label_lengths, 1, "label_lenghts")
max_T = torch.max(lengths)
max_U = torch.max(label_lengths)
T, U = log_probs.shape[1:3]
if T != max_T:
raise ValueError(f"Input length mismatch! Given T: {T}, Expected max T from input lengths: {max_T}")
if U != max_U + 1:
raise ValueError(f"Output length mismatch! Given U: {U}, Expected max U from target lengths: {max_U} + 1")
@@ -0,0 +1,13 @@
# Copyright (c) 2021, 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.
@@ -0,0 +1,27 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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.
@@ -0,0 +1,378 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 math
import multiprocessing
from typing import Optional
import numba
import torch
from nemo.collections.asr.parts.numba.rnnt_loss.utils import global_constants
def log_sum_exp(a: torch.Tensor, b: torch.Tensor):
"""
Logsumexp with safety checks for infs.
"""
if torch.isinf(a):
return b
if torch.isinf(b):
return a
if a > b:
return math.log1p(math.exp(b - a)) + a
else:
return math.log1p(math.exp(a - b)) + b
class CpuRNNT_index:
def __init__(self, U: int, maxU: int, minibatch: int, alphabet_size: int, batch_first: bool):
"""
A placeholder Index computation class that emits the resolved index in a flattened tensor,
mimicing pointer indexing in CUDA kernels on the CPU.
Args:
U: Length of the current target sample (without padding).
maxU: Max Length of the padded target samples.
minibatch: Minibatch index
alphabet_size: Size of the vocabulary including RNNT blank - V+1.
batch_first: Bool flag determining if batch index is first or third.
"""
super(CpuRNNT_index, self).__init__()
self.U = U
self.maxU = maxU
self.minibatch = minibatch
self.alphabet_size = alphabet_size
self.batch_first = batch_first
def __call__(self, t: int, u: int, v: Optional[int] = None):
# if indexing all the values of the vocabulary, then only t, u are provided
if v is None:
return t * self.U + u
else:
# otherwise, t, u, v are provided to index particular value in the vocabulary.
if self.batch_first:
return (t * self.maxU + u) * self.alphabet_size + v
else:
return (t * self.maxU + u) * self.minibatch * self.alphabet_size + v
class CpuRNNT_metadata:
def __init__(
self,
T: int,
U: int,
workspace: torch.Tensor,
bytes_used: int,
blank: int,
labels: torch.Tensor,
log_probs: torch.Tensor,
idx: CpuRNNT_index,
):
"""
Metadata for CPU based RNNT loss calculation. Holds the working space memory.
Args:
T: Length of the acoustic sequence (without padding).
U: Length of the target sequence (without padding).
workspace: Working space memory for the CPU.
bytes_used: Number of bytes currently used for indexing the working space memory. Generally 0.
blank: Index of the blank token in the vocabulary.
labels: Ground truth padded labels matrix of shape [B, U]
log_probs: Log probs / activation matrix of flattented shape [B, T, U, V+1]
idx:
"""
super(CpuRNNT_metadata, self).__init__()
self.alphas = workspace[bytes_used : bytes_used + T * U]
bytes_used += T * U
self.betas = workspace[bytes_used : bytes_used + T * U]
bytes_used += T * U
self.log_probs2 = workspace[bytes_used : bytes_used + T * U * 2] # // only store blank & label
bytes_used += T * U * 2
self.bytes_used = bytes_used
self.setup_probs(T, U, labels, blank, log_probs, idx)
def setup_probs(
self, T: int, U: int, labels: torch.Tensor, blank: int, log_probs: torch.Tensor, idx: CpuRNNT_index
):
# initialize the log probs memory for blank and label token.
for t in range(T):
for u in range(U):
offset = (t * U + u) * 2 # mult with 2 is for selecting either blank or label token. Odd idx is blank.
self.log_probs2[offset] = log_probs[idx(t, u, blank)]
# // labels do not have first blank
if u < U - 1:
self.log_probs2[offset + 1] = log_probs[idx(t, u, labels[u])]
class CPURNNT:
def __init__(
self,
minibatch: int,
maxT: int,
maxU: int,
alphabet_size: int,
workspace: torch.Tensor,
blank: int,
num_threads: int,
batch_first: bool,
):
"""
Helper class to compute the Transducer Loss on CPU.
Args:
minibatch: Size of the minibatch b.
maxT: The maximum possible acoustic sequence length. Represents T in the logprobs tensor.
maxU: The maximum possible target sequence length. Represents U in the logprobs tensor.
alphabet_size: The vocabulary dimension V+1 (inclusive of RNNT blank).
workspace: An allocated chunk of memory that will be sliced off and reshaped into required
blocks used as working memory.
blank: Index of the RNNT blank token in the vocabulary. Generally the first or last token in the vocab.
num_threads: Number of OMP threads to launch.
batch_first: Bool that decides if batch dimension is first or third.
"""
self.minibatch_ = minibatch
self.maxT_ = maxT
self.maxU_ = maxU
self.alphabet_size_ = alphabet_size
self.workspace = workspace # a flat vector of floatX numbers that represents allocated memory slices
self.blank_ = blank
self.num_threads_ = num_threads
self.batch_first = batch_first
if num_threads > 0:
numba.set_num_threads(min(multiprocessing.cpu_count(), num_threads))
else:
self.num_threads_ = numba.get_num_threads()
def cost_and_grad_kernel(
self,
log_probs: torch.Tensor,
grad: torch.Tensor,
labels: torch.Tensor,
mb: int,
T: int,
U: int,
bytes_used: int,
):
idx = CpuRNNT_index(U, self.maxU_, self.minibatch_, self.alphabet_size_, self.batch_first)
rnntm = CpuRNNT_metadata(T, U, self.workspace, bytes_used, self.blank_, labels, log_probs, idx)
if self.batch_first:
# zero grads
grad *= 0.0
llForward = self.compute_alphas(rnntm.log_probs2, T, U, rnntm.alphas)
llBackward = self.compute_betas_and_grads(
grad, rnntm.log_probs2, T, U, rnntm.alphas, rnntm.betas, labels, llForward
)
diff = (llForward - llBackward).abs()
if diff > 0.1:
print(f"WARNING: Forward backward likelihood mismatch : {diff}")
return -llForward
def compute_alphas(self, log_probs: torch.Tensor, T: int, U: int, alphas: torch.Tensor):
"""
Compute the probability of the forward variable alpha.
Args:
log_probs: Flattened tensor [B, T, U, V+1]
T: Length of the acoustic sequence T (not padded).
U: Length of the target sequence U (not padded).
alphas: Working space memory for alpha of shape [B, T, U].
Returns:
Loglikelihood of the forward variable alpha.
"""
idx = CpuRNNT_index(U, self.maxU_, self.minibatch_, self.alphabet_size_, self.batch_first)
alphas[0] = 0
for t in range(T):
for u in range(U):
if u == 0 and t > 0:
alphas[idx(t, 0)] = alphas[idx(t - 1, 0)] + log_probs[idx(t - 1, 0) * 2]
if t == 0 and u > 0:
alphas[idx(0, u)] = alphas[idx(0, u - 1)] + log_probs[idx(0, u - 1) * 2 + 1]
if t > 0 and u > 0:
no_emit = alphas[idx(t - 1, u)] + log_probs[idx(t - 1, u) * 2]
emit = alphas[idx(t, u - 1)] + log_probs[idx(t, u - 1) * 2 + 1]
alphas[idx(t, u)] = log_sum_exp(emit, no_emit)
loglike = alphas[idx(T - 1, U - 1)] + log_probs[idx(T - 1, U - 1) * 2]
return loglike
def compute_betas_and_grads(
self,
grad: torch.Tensor,
log_probs: torch.Tensor,
T: int,
U: int,
alphas: torch.Tensor,
betas: torch.Tensor,
labels: torch.Tensor,
logll: torch.Tensor,
):
"""
Compute backward variable beta as well as gradients of the activation matrix wrt loglikelihood
of forward variable.
Args:
grad: Working space memory of flattened shape [B, T, U, V+1]
log_probs: Activatio tensor of flattented shape [B, T, U, V+1]
T: Length of the acoustic sequence T (not padded).
U: Length of the target sequence U (not padded).
alphas: Working space memory for alpha of shape [B, T, U].
betas: Working space memory for alpha of shape [B, T, U].
labels: Ground truth label of shape [B, U]
logll: Loglikelihood of the forward variable.
Returns:
Loglikelihood of the forward variable and inplace updates the grad tensor.
"""
idx = CpuRNNT_index(U, self.maxU_, self.minibatch_, self.alphabet_size_, self.batch_first)
betas[idx(T - 1, U - 1)] = log_probs[idx(T - 1, U - 1) * 2]
for t in range(T - 1, -1, -1):
for u in range(U - 1, -1, -1):
if (u == U - 1) and (t < T - 1):
betas[idx(t, U - 1)] = betas[idx(t + 1, U - 1)] + log_probs[idx(t, U - 1) * 2]
if (t == T - 1) and (u < U - 1):
betas[idx(T - 1, u)] = betas[idx(T - 1, u + 1)] + log_probs[idx(T - 1, u) * 2 + 1]
if (t < T - 1) and (u < U - 1):
no_emit = betas[idx(t + 1, u)] + log_probs[idx(t, u) * 2]
emit = betas[idx(t, u + 1)] + log_probs[idx(t, u) * 2 + 1]
betas[idx(t, u)] = log_sum_exp(emit, no_emit)
loglike = betas[0]
# // Gradients w.r.t. log probabilities
for t in range(T):
for u in range(U):
if t < T - 1:
g = alphas[idx(t, u)] + betas[idx(t + 1, u)]
grad[idx(t, u, self.blank_)] = -torch.exp(log_probs[idx(t, u) * 2] + g - loglike)
if u < U - 1:
g = alphas[idx(t, u)] + betas[idx(t, u + 1)]
grad[idx(t, u, labels[u])] = -torch.exp(log_probs[idx(t, u) * 2 + 1] + g - loglike)
# // gradient to the last blank transition
grad[idx(T - 1, U - 1, self.blank_)] = -torch.exp(
log_probs[idx(T - 1, U - 1) * 2] + alphas[idx(T - 1, U - 1)] - loglike
)
return loglike
def cost_and_grad(
self,
log_probs: torch.Tensor,
grads: torch.Tensor,
costs: torch.Tensor,
flat_labels: torch.Tensor,
label_lengths: torch.Tensor,
input_lengths: torch.Tensor,
) -> global_constants.RNNTStatus:
# // per minibatch memory
per_minibatch_bytes = 0
# // alphas & betas
per_minibatch_bytes += self.maxT_ * self.maxU_ * 2
# // blank & label log probability cache
per_minibatch_bytes += self.maxT_ * self.maxU_ * 2
for mb in range(self.minibatch_):
T = input_lengths[mb] # // Length of utterance (time)
U = label_lengths[mb] + 1 # // Number of labels in transcription
batch_size = self.alphabet_size_
if self.batch_first:
batch_size = self.maxT_ * self.maxU_ * self.alphabet_size_
costs[mb] = self.cost_and_grad_kernel(
log_probs[(mb * batch_size) :],
grads[(mb * batch_size) :],
flat_labels[(mb * (self.maxU_ - 1)) :],
mb,
T,
U,
mb * per_minibatch_bytes,
)
return global_constants.RNNTStatus.RNNT_STATUS_SUCCESS
def score_forward(
self,
log_probs: torch.Tensor,
costs: torch.Tensor,
flat_labels: torch.Tensor,
label_lengths: torch.Tensor,
input_lengths: torch.Tensor,
):
# // per minibatch memory
per_minibatch_bytes = 0
# // alphas & betas
per_minibatch_bytes += self.maxT_ * self.maxU_ * 2
# // blank & label log probability cache
per_minibatch_bytes += self.maxT_ * self.maxU_ * 2
for mb in range(self.minibatch_):
T = input_lengths[mb] # // Length of utterance (time)
U = label_lengths[mb] + 1 # // Number of labels in transcription
batch_size = self.alphabet_size_
if self.batch_first:
batch_size = self.maxT_ * self.maxU_ * self.alphabet_size_
idx = CpuRNNT_index(U, self.maxU_, self.minibatch_, self.alphabet_size_, self.batch_first)
rnntm = CpuRNNT_metadata(
T,
U,
self.workspace,
mb * per_minibatch_bytes,
self.blank_,
flat_labels[(mb * (self.maxU_ - 1)) :],
log_probs[(mb * batch_size) :],
idx,
)
costs[mb] = -self.compute_alphas(rnntm.log_probs2, T, U, rnntm.alphas)
return global_constants.RNNTStatus.RNNT_STATUS_SUCCESS
@@ -0,0 +1,27 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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.
@@ -0,0 +1,254 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 multiprocessing
from typing import Optional
import numba
import torch
from numba import cuda
from nemo.collections.asr.parts.numba.rnnt_loss.utils import global_constants
from nemo.collections.asr.parts.numba.rnnt_loss.utils.cuda_utils import gpu_rnnt_kernel, reduce
class GPURNNT:
def __init__(
self, minibatch: int, maxT: int, maxU: int, alphabet_size: int, workspace, blank: int, num_threads: int, stream
):
"""
Helper class to launch the CUDA Kernels to compute the Transducer Loss.
Args:
minibatch: Int representing the batch size.
maxT: The maximum possible acoustic sequence length. Represents T in the logprobs tensor.
maxU: The maximum possible target sequence length. Represents U in the logprobs tensor.
alphabet_size: The vocabulary dimension V+1 (inclusive of RNNT blank).
workspace: An allocated chunk of memory that will be sliced off and reshaped into required
blocks used as working memory.
blank: Index of the RNNT blank token in the vocabulary. Generally the first or last token in the vocab.
num_threads: Number of OMP threads to launch.
stream: Numba Cuda Stream.
"""
self.minibatch_ = minibatch
self.maxT_ = maxT
self.maxU_ = maxU
self.alphabet_size_ = alphabet_size
self.gpu_workspace = cuda.as_cuda_array(
workspace
) # a flat vector of floatX numbers that represents allocated memory slices
self.blank_ = blank
self.num_threads_ = num_threads
self.stream_ = stream # type: cuda.cudadrv.driver.Stream
if num_threads > 0:
numba.set_num_threads(min(multiprocessing.cpu_count(), num_threads))
else:
self.num_threads_ = numba.get_num_threads()
def log_softmax(self, acts: torch.Tensor, denom: torch.Tensor):
"""
Computes the log softmax denominator of the input activation tensor
and stores the result in denom.
Args:
acts: Activation tensor of shape [B, T, U, V+1]. The input must be represented as a flat tensor
of shape [B * T * U * (V+1)] to allow pointer indexing.
denom: A zero tensor of same shape as acts.
Updates:
This kernel inplace updates the `denom` tensor
"""
# // trans_acts + pred_acts -> log_softmax denominator
reduce.reduce_max(
acts,
denom,
rows=self.alphabet_size_,
cols=self.minibatch_ * self.maxT_ * self.maxU_,
minus=False,
stream=self.stream_,
)
reduce.reduce_exp(
acts,
denom,
rows=self.alphabet_size_,
cols=self.minibatch_ * self.maxT_ * self.maxU_,
minus=True,
stream=self.stream_,
)
def compute_cost_and_score(
self,
acts: torch.Tensor,
grads: Optional[torch.Tensor],
costs: torch.Tensor,
labels: torch.Tensor,
label_lengths: torch.Tensor,
input_lengths: torch.Tensor,
) -> global_constants.RNNTStatus:
"""
Compute both the loss and the gradients.
Args:
acts: A flattened tensor of shape [B, T, U, V+1] representing the activation matrix.
grad: A flattented zero tensor of same shape as acts.
costs: A zero vector of length B which will be updated inplace with the log probability costs.
flat_labels: A flattened matrix of labels of shape [B, U]
label_lengths: A vector of length B that contains the original lengths of the acoustic sequence.
input_lengths: A vector of length B that contains the original lengths of the target sequence.
Updates:
This will launch kernels that will update inline the following variables:
- grads: Gradients of the activation matrix wrt the costs vector.
- costs: Negative log likelihood of the forward variable.
Returns:
An enum that either represents a successful RNNT operation or failure.
"""
training = grads is not None
used_offset = 0
# // denom
denom = self.gpu_workspace[used_offset : used_offset + self.maxT_ * self.maxU_ * self.minibatch_]
used_offset += self.maxT_ * self.maxU_ * self.minibatch_
# // alphas & betas
alphas = self.gpu_workspace[used_offset : used_offset + self.maxT_ * self.maxU_ * self.minibatch_]
used_offset += self.maxT_ * self.maxU_ * self.minibatch_
betas = self.gpu_workspace[used_offset : used_offset + self.maxT_ * self.maxU_ * self.minibatch_]
used_offset += self.maxT_ * self.maxU_ * self.minibatch_
# // logllh
llForward = self.gpu_workspace[used_offset : used_offset + self.minibatch_]
used_offset += self.minibatch_
llBackward = self.gpu_workspace[used_offset : used_offset + self.minibatch_]
used_offset += self.minibatch_
if training:
grads *= 0.0 # zero grads
######## START EXECUTION ########
self.log_softmax(acts, denom)
# Compute alphas
gpu_rnnt_kernel.compute_alphas_kernel[self.minibatch_, self.maxU_, self.stream_, 0](
acts,
denom,
alphas,
llForward,
input_lengths,
label_lengths,
labels,
self.minibatch_,
self.maxT_,
self.maxU_,
self.alphabet_size_,
self.blank_,
)
if training:
# Compute betas
gpu_rnnt_kernel.compute_betas_kernel[self.minibatch_, self.maxU_, self.stream_, 0](
acts,
denom,
betas,
llBackward,
input_lengths,
label_lengths,
labels,
self.minibatch_,
self.maxT_,
self.maxU_,
self.alphabet_size_,
self.blank_,
)
# Compute gradient
grad_blocks_per_grid = self.minibatch_ * self.maxT_ * self.maxU_
grad_threads_per_block = gpu_rnnt_kernel.GPU_RNNT_THREAD_SIZE
gpu_rnnt_kernel.compute_grad_kernel[grad_blocks_per_grid, grad_threads_per_block, self.stream_, 0](
grads,
acts,
denom,
alphas,
betas,
llForward,
input_lengths,
label_lengths,
labels,
self.minibatch_,
self.maxT_,
self.maxU_,
self.alphabet_size_,
self.blank_,
)
# // cost
costs.copy_to_device(llForward, stream=self.stream_)
self.stream_.synchronize()
# compute negative log likelihood.
for mb in range(self.minibatch_):
costs[mb] = -costs[mb]
return global_constants.RNNTStatus.RNNT_STATUS_SUCCESS
def cost_and_grad(
self,
acts: torch.Tensor,
grads: torch.Tensor,
costs: torch.Tensor,
pad_labels: torch.Tensor,
label_lengths: torch.Tensor,
input_lengths: torch.Tensor,
):
if (
acts is None
or grads is None
or costs is None
or pad_labels is None
or label_lengths is None
or input_lengths is None
):
return global_constants.RNNTStatus.RNNT_STATUS_INVALID_VALUE
return self.compute_cost_and_score(acts, grads, costs, pad_labels, label_lengths, input_lengths)
def score_forward(
self,
acts: torch.Tensor,
costs: torch.Tensor,
pad_labels: torch.Tensor,
label_lengths: torch.Tensor,
input_lengths: torch.Tensor,
):
if acts is None or costs is None or pad_labels is None or label_lengths is None or input_lengths is None:
return global_constants.RNNTStatus.RNNT_STATUS_INVALID_VALUE
return self.compute_cost_and_score(acts, None, costs, pad_labels, label_lengths, input_lengths)
@@ -0,0 +1,365 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 math
import torch
from numba import cuda
from nemo.collections.asr.parts.numba.rnnt_loss.utils import rnnt_helper
GPU_RNNT_THREAD_SIZE = 128
@cuda.jit(device=True, inline=True)
def logp(
denom: torch.Tensor, acts: torch.Tensor, maxT: int, maxU: int, alphabet_size: int, mb: int, t: int, u: int, v: int
):
"""
Compute the sum of log probability from the activation tensor and its denominator.
Args:
denom: Tensor of shape [B, T, U] flattened. Represents the denominator of the logprobs activation tensor
across entire vocabulary.
acts: Tensor of shape [B, T, U, V+1] flattened. Represents the logprobs activation tensor.
maxT: The maximum possible acoustic sequence length. Represents T in the logprobs tensor.
maxU: The maximum possible target sequence length. Represents U in the logprobs tensor.
alphabet_size: The vocabulary dimension V+1 (inclusive of RNNT blank).
mb: Batch indexer.
t: Acoustic sequence timestep indexer.
u: Target sequence timestep indexer.
v: Vocabulary token indexer.
Returns:
The sum of logprobs[mb, t, u, v] + denom[mb, t, u]
"""
col = (mb * maxT + t) * maxU + u
return denom[col] + acts[col * alphabet_size + v]
@cuda.jit()
def compute_alphas_kernel(
acts: torch.Tensor,
denom: torch.Tensor,
alphas: torch.Tensor,
llForward: torch.Tensor,
xlen: torch.Tensor,
ylen: torch.Tensor,
mlabels: torch.Tensor, # [B]
minibatch: int,
maxT: int,
maxU: int,
alphabet_size: int,
blank_: int,
):
"""
Compute alpha (forward variable) probabilities over the transduction step.
Args:
acts: Tensor of shape [B, T, U, V+1] flattened. Represents the logprobs activation tensor.
denom: Tensor of shape [B, T, U] flattened. Represents the denominator of the logprobs activation tensor
across entire vocabulary.
alphas: Zero tensor of shape [B, T, U]. Will be updated inside the kernel with the forward variable
probabilities.
llForward: Zero tensor of shape [B]. Represents the log-likelihood of the forward pass.
Returned as the forward pass loss that is reduced by the optimizer.
xlen: Vector of length B which contains the actual acoustic sequence lengths in the padded
activation tensor.
ylen: Vector of length B which contains the actual target sequence lengths in the padded
activation tensor.
mlabels: Matrix of shape [B, U+1] (+1 here is due to <SOS> token - usually the RNNT blank).
The matrix contains the padded target transcription that must be predicted.
minibatch: Int representing the batch size.
maxT: The maximum possible acoustic sequence length. Represents T in the logprobs tensor.
maxU: The maximum possible target sequence length. Represents U in the logprobs tensor.
alphabet_size: The vocabulary dimension V+1 (inclusive of RNNT blank).
blank_: Index of the RNNT blank token in the vocabulary. Generally the first or last token in the vocab.
Updates:
Kernel inplace updates the following inputs:
- alphas: forward variable scores.
- llForward: log-likelihood of forward variable.
"""
# // launch B blocks, each block has U threads
b = cuda.blockIdx.x # // batch id
u = cuda.threadIdx.x # label id, u
T = xlen[b] # select AM length of current sample
U = ylen[b] + 1 # select target length of current sample, +1 for the blank token
labels: torch.Tensor = mlabels[b] # mb label start point, equivalent to mlabels + b * (maxU - 1)
offset = b * maxT * maxU # pointer indexing offset
# alphas += offset # pointer offset, ignored since we explicitly add offset
# Initilize alpha[b, t=0, u=0] for all b in B
if u == 0:
alphas[offset] = 0
# sync until all alphas are initialized
cuda.syncthreads()
# Ordinary alpha calculations, broadcast across B=b and U=u
# Look up forward variable calculation from rnnt_numpy.forward_pass()
for n in range(1, T + U - 1):
t = n - u
if u == 0:
# for t in range(1, T) step to initialize alphas[b, t, 0]
if t > 0 and t < T:
alphas[offset + t * maxU + u] = alphas[offset + (t - 1) * maxU + u] + logp(
denom, acts, maxT, maxU, alphabet_size, b, t - 1, 0, blank_
)
elif u < U:
# for u in range(1, U) step to initialize alphas[b, 0, u]
if t == 0:
alphas[offset + u] = alphas[offset + u - 1] + logp(
denom, acts, maxT, maxU, alphabet_size, b, 0, u - 1, labels[u - 1]
)
# for t in range(1, T) for u in range(1, U) step to compute alphas[b, t, u]
elif t > 0 and t < T:
no_emit = alphas[offset + (t - 1) * maxU + u] + logp(
denom, acts, maxT, maxU, alphabet_size, b, t - 1, u, blank_
)
emit = alphas[offset + t * maxU + u - 1] + logp(
denom, acts, maxT, maxU, alphabet_size, b, t, u - 1, labels[u - 1]
)
alphas[offset + t * maxU + u] = rnnt_helper.log_sum_exp(emit, no_emit)
# sync across all B=b and U=u
cuda.syncthreads()
# After final sync, alphas[b, T-1, U - 1] + logprobs[b, T-1, U-1, blank] + denom[b, T-1, U-1] gives
# log-likelihood of forward pass.
if u == 0:
loglike = alphas[offset + (T - 1) * maxU + U - 1] + logp(
denom, acts, maxT, maxU, alphabet_size, b, T - 1, U - 1, blank_
)
llForward[b] = loglike
@cuda.jit()
def compute_betas_kernel(
acts: torch.Tensor,
denom: torch.Tensor,
betas: torch.Tensor,
llBackward: torch.Tensor,
xlen: torch.Tensor,
ylen: torch.Tensor,
mlabels: torch.Tensor, # [B, U]
minibatch: int,
maxT: int,
maxU: int,
alphabet_size: int,
blank_: int,
):
"""
Compute beta (backward variable) probabilities over the transduction step.
Args:
acts: Tensor of shape [B, T, U, V+1] flattened. Represents the logprobs activation tensor.
denom: Tensor of shape [B, T, U] flattened. Represents the denominator of the logprobs activation tensor
across entire vocabulary.
betas: Zero tensor of shape [B, T, U]. Will be updated inside the kernel with the backward variable
probabilities.
llBackward: Zero tensor of shape [B]. Represents the log-likelihood of the backward pass.
Returned as the backward pass loss that is reduced by the optimizer.
xlen: Vector of length B which contains the actual acoustic sequence lengths in the padded
activation tensor.
ylen: Vector of length B which contains the actual target sequence lengths in the padded
activation tensor.
mlabels: Matrix of shape [B, U+1] (+1 here is due to <SOS> token - usually the RNNT blank).
The matrix contains the padded target transcription that must be predicted.
minibatch: Int representing the batch size.
maxT: The maximum possible acoustic sequence length. Represents T in the logprobs tensor.
maxU: The maximum possible target sequence length. Represents U in the logprobs tensor.
alphabet_size: The vocabulary dimension V+1 (inclusive of RNNT blank).
blank_: Index of the RNNT blank token in the vocabulary. Generally the first or last token in the vocab.
Updates:
Kernel inplace updates the following inputs:
- betas: backward variable scores.
- llBackward: log-likelihood of backward variable.
"""
# // launch B blocks, each block has U threads
b = cuda.blockIdx.x # // batch id
u = cuda.threadIdx.x # label id, u
T = xlen[b] # select AM length of current sample
U = ylen[b] + 1 # select target length of current sample, +1 for the blank token
labels: torch.Tensor = mlabels[b] # mb label start point, equivalent to mlabels + b * (maxU - 1)
offset = b * maxT * maxU # pointer indexing offset
# betas += offset # pointer offset, ignored since we explicitly add offset
# Initilize beta[b, t=T-1, u=U-1] for all b in B with log_probs[b, t=T-1, u=U-1, blank]
if u == 0:
betas[offset + (T - 1) * maxU + U - 1] = logp(denom, acts, maxT, maxU, alphabet_size, b, T - 1, U - 1, blank_)
# sync until all betas are initialized
cuda.syncthreads()
# Ordinary beta calculations, broadcast across B=b and U=u
# Look up backward variable calculation from rnnt_numpy.backward_pass()
for n in range(T + U - 2, -1, -1):
t = n - u
if u == (U - 1):
# for t in reversed(range(T - 1)) step to initialize betas[b, t, U-1]
if t >= 0 and t < (T - 1):
betas[offset + t * maxU + U - 1] = betas[offset + (t + 1) * maxU + U - 1] + logp(
denom, acts, maxT, maxU, alphabet_size, b, t, U - 1, blank_
)
elif u < U:
if t == T - 1:
# for u in reversed(range(U - 1)) step to initialize betas[b, T-1, u]
betas[offset + (T - 1) * maxU + u] = betas[offset + (T - 1) * maxU + u + 1] + logp(
denom, acts, maxT, maxU, alphabet_size, b, T - 1, u, labels[u]
)
elif (t >= 0) and (t < T - 1):
# for t in reversed(range(T - 1)) for u in reversed(range(U - 1)) step to compute betas[b, t, u]
no_emit = betas[offset + (t + 1) * maxU + u] + logp(
denom, acts, maxT, maxU, alphabet_size, b, t, u, blank_
)
emit = betas[offset + t * maxU + u + 1] + logp(
denom, acts, maxT, maxU, alphabet_size, b, t, u, labels[u]
)
betas[offset + t * maxU + u] = rnnt_helper.log_sum_exp(emit, no_emit)
# sync across all B=b and U=u
cuda.syncthreads()
# After final sync, betas[b, 0, 0] gives
# log-likelihood of backward pass.
if u == 0:
llBackward[b] = betas[offset]
@cuda.jit()
def compute_grad_kernel(
grads: torch.Tensor,
acts: torch.Tensor,
denom: torch.Tensor,
alphas: torch.Tensor,
betas: torch.Tensor,
logll: torch.Tensor,
xlen: torch.Tensor,
ylen: torch.Tensor,
mlabels: torch.Tensor, # [B, U]
minibatch: int,
maxT: int,
maxU: int,
alphabet_size: int,
blank_: int,
):
"""
Compute gradients over the transduction step.
Args:
grads: Zero Tensor of shape [B, T, U, V+1]. Is updated by this kernel to contain the gradients
of this batch of samples.
acts: Tensor of shape [B, T, U, V+1] flattened. Represents the logprobs activation tensor.
denom: Tensor of shape [B, T, U] flattened. Represents the denominator of the logprobs activation tensor
across entire vocabulary.
alphas: Alpha variable, contains forward probabilities. A tensor of shape [B, T, U].
betas: Beta varoable, contains backward probabilities. A tensor of shape [B, T, U].
logll: Log-likelihood of the forward variable, represented as a vector of shape [B].
Represents the log-likelihood of the forward pass.
xlen: Vector of length B which contains the actual acoustic sequence lengths in the padded
activation tensor.
ylen: Vector of length B which contains the actual target sequence lengths in the padded
activation tensor.
mlabels: Matrix of shape [B, U+1] (+1 here is due to <SOS> token - usually the RNNT blank).
The matrix contains the padded target transcription that must be predicted.
minibatch: Int representing the batch size.
maxT: The maximum possible acoustic sequence length. Represents T in the logprobs tensor.
maxU: The maximum possible target sequence length. Represents U in the logprobs tensor.
alphabet_size: The vocabulary dimension V+1 (inclusive of RNNT blank).
blank_: Index of the RNNT blank token in the vocabulary. Generally the first or last token in the vocab.
Updates:
Kernel inplace updates the following inputs:
- grads: Gradients with respect to the log likelihood (logll).
"""
# Kernel call:
# blocks_per_grid = minibatch (b) * maxT (t) * maxU (u)
# threads_per_block = constant buffer size of parallel threads (v :: Constant)
tid = cuda.threadIdx.x # represents v, taking steps of some constant size
idx = tid # index of v < V+1; in steps of constant buffer size
col = cuda.blockIdx.x # represents a fused index of b * t * u
# Decompose original indices from fused `col`
u = col % maxU # (b * t * u) % u = u
bt = (col - u) // maxU # (b * t * u - u) // U = b * t
t = bt % maxT # (b * t) % t = t
mb = (bt - t) // maxT # (b * t - t) // T = b
# constants
T = xlen[mb] # select AM length of current sample
U = ylen[mb] + 1 # select target length of current sample, +1 for the blank token
labels: torch.Tensor = mlabels[mb] # labels = mlabels + mb * (maxU - 1);
# Buffered gradient calculations, broadcast across B=b, T=t and U=u, looped over V with some constant stride.
# Look up gradient calculation from rnnt_numpy.compute_gradient()
if t < T and u < U:
# For cuda kernels, maximum number of threads per block is limited to some value.
# However, it may be the case that vocabulary size is larger than this limit
# To work around this, an arbitrary thread buffer size is chosen such that,
# 1) each element within the thread pool operates independently of the other
# 2) An inner while loop moves the index of each buffer element by the size of the buffer itself,
# such that all elements of the vocabulary size are covered in (V + 1 // thread_buffer) number of steps.
# As such, each thread will perform the while loop at least (V + 1 // thread_buffer) number of times
while idx < alphabet_size:
# remember, `col` represents the tri-index [b, t, u]
# therefore; logpk = denom[b, t, u] + acts[b, t, u, v]
logpk = denom[col] + acts[col * alphabet_size + idx]
# initialize the grad of the sample acts[b, t, u, v]
grad = math.exp(alphas[col] + betas[col] + logpk - logll[mb])
# // grad to last blank transition
# grad[b, T-1, U-1, v=blank] -= exp(alphas[b, t, u) + logpk - logll[b])
if (idx == blank_) and (t == T - 1) and (u == U - 1):
grad -= math.exp(alphas[col] + logpk - logll[mb])
# grad of blank across t < T;
# grad[b, t<T-1, u, v=blank] -= exp(alphas[b, t, u] + logpk - logll[b] betas[b, t + 1, u])
if (idx == blank_) and (t < T - 1):
grad -= math.exp(alphas[col] + logpk - logll[mb] + betas[col + maxU])
# grad of correct token across u < U;
# grad[b, t, u<U-1, v=label[u]] -= exp(alphas[b, t, u] + logpk - logll[b] + betas[b, t, u+1])
if (u < U - 1) and (idx == labels[u]):
grad -= math.exp(alphas[col] + logpk - logll[mb] + betas[col + 1])
# update grads[b, t, u, v] = grad
grads[col * alphabet_size + idx] = grad
# update internal index through the thread_buffer;
# until idx < V + 1, such that entire vocabulary has been updated.
idx += GPU_RNNT_THREAD_SIZE
@@ -0,0 +1,362 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 enum
import math
import torch
from numba import cuda
from nemo.collections.asr.parts.numba.rnnt_loss.utils import global_constants, rnnt_helper
warp_size = global_constants.warp_size()
dtype = global_constants.dtype()
CTA_REDUCE_SIZE = 128
class I_Op(enum.Enum):
"""
Represents an operation that is performed on the input tensor
"""
EXPONENTIAL = 0
IDENTITY = 1
class R_Op(enum.Enum):
"""
Represents a reduction operation performed on the input tensor
"""
ADD = 0
MAXIMUM = 1
@cuda.jit(device=True)
def CTAReduce(tid: int, x, storage, count: int, R_opid: int):
"""
CUDA Warp reduction kernel.
It is a device kernel to be called by other kernels.
The data will be read from the right segement recursively, and reduced (ROP) onto the left half.
Operation continues while warp size is larger than a given offset.
Beyond this offset, warp reduction is performed via `shfl_down_sync`, which halves the reduction
space and sums the two halves at each call.
Note:
Efficient warp occurs at input shapes of 2 ^ K.
References:
- Warp Primitives [https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/]
Args:
tid: CUDA thread index
x: activation. Single float.
storage: shared memory of size CTA_REDUCE_SIZE used for reduction in parallel threads.
count: equivalent to num_rows, which is equivalent to alphabet_size (V+1)
R_opid: Operator ID for reduction. See R_Op for more information.
"""
storage[tid] = x
cuda.syncthreads()
# Fold the data in half with each pass
offset = CTA_REDUCE_SIZE // 2
while offset >= warp_size:
if (tid + offset) < count and tid < offset:
# Read from the right half and store to the left half.
if R_opid == 0:
x = rnnt_helper.add(x, storage[offset + tid])
else:
x = rnnt_helper.maximum(x, storage[offset + tid])
storage[tid] = x
cuda.syncthreads()
offset = offset // 2
offset = warp_size // 2
while offset > 0:
# warp reduction and sync
shuff = cuda.shfl_down_sync(0xFFFFFFFF, x, offset)
if (tid + offset < count) and (tid < offset):
if R_opid == 0:
x = rnnt_helper.add(x, shuff)
else:
x = rnnt_helper.maximum(x, shuff)
offset = offset // 2
return x
@cuda.jit()
def _reduce_rows(I_opid: int, R_opid: int, acts, output, num_rows: int):
"""
CUDA Warp reduction kernel which reduces via the R_Op.Maximum
Reduces the input data such that I_Op = Identity and R_op = Maximum.
The result is stored in the blockIdx, and is stored as an identity op.
Note:
Efficient warp occurs at input shapes of 2 ^ K.
References:
- Warp Primitives [https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/]
Args:
I_opid: Operator ID for input. See I_Op for more information. For this kernel,
the Identity op is chosen in general, and therefore the input is reduced in place
without scaling.
R_opid: Operator ID for reduction. See R_Op for more information.
For this kernel, generally Maximum op is chosen. It reduces the kernel via max.
acts: Flatened activation matrix of shape [B * T * U * (V+1)].
output: Flatened output matrix of shape [B * T * U * (V+1)]. Data will be overwritten.
num_rows: Vocabulary size (including blank token) - V+1.
"""
tid = cuda.threadIdx.x
idx = tid
col = cuda.blockIdx.x
# allocate shared thread memory
storage = cuda.shared.array(shape=(CTA_REDUCE_SIZE,), dtype=acts.dtype)
max = output[col]
# // Each block works on a column
if idx < num_rows:
curr = acts[col * num_rows + idx] - max
if I_opid == 0:
curr = rnnt_helper.exponential(curr)
else:
curr = rnnt_helper.identity(curr)
idx += CTA_REDUCE_SIZE
while idx < num_rows:
activation_ = acts[col * num_rows + idx] - max
if I_opid == 0 and R_opid == 0:
curr = rnnt_helper.add(curr, rnnt_helper.exponential(activation_))
elif I_opid == 0 and R_opid == 1:
curr = rnnt_helper.maximum(curr, rnnt_helper.exponential(activation_))
elif I_opid == 1 and R_opid == 0:
curr = rnnt_helper.add(curr, rnnt_helper.identity(activation_))
else:
curr = rnnt_helper.maximum(curr, rnnt_helper.identity(activation_))
idx += CTA_REDUCE_SIZE
# // Sum thread-totals over the CTA.
curr = CTAReduce(tid, curr, storage, num_rows, R_opid)
# // Store result in out (inplace, I_op: identity)
if tid == 0:
output[col] = curr
@cuda.jit()
def _reduce_minus(I_opid: int, R_opid: int, acts, output, num_rows: int):
"""
CUDA Warp reduction kernel which reduces via the R_Op.Add
Reduces the input data such that I_Op = Exponential and R_op = Add.
The result is stored in the blockIdx, and is stored as an exp op.
Note:
Efficient warp occurs at input shapes of 2 ^ K.
References:
- Warp Primitives [https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/]
Args:
I_opid: Operator ID for input. See I_Op for more information. For this kernel,
the Exponential op is chosen in general, and therefore the input is reduced in place
with scaling.
R_opid: Operator ID for reduction. See R_Op for more information.
For this kernel, generally Add op is chosen. It reduces the kernel via summation.
acts: Flatened activation matrix of shape [B * T * U * (V+1)].
output: Flatened output matrix of shape [B * T * U * (V+1)]. Data will be overwritten.
num_rows: Vocabulary size (including blank token) - V+1.
"""
tid = cuda.threadIdx.x
idx = tid
col = cuda.blockIdx.x
# allocate shared thread memory
storage = cuda.shared.array(shape=(CTA_REDUCE_SIZE,), dtype=acts.dtype)
max = output[col]
# // Each block works on a column
if idx < num_rows:
curr = acts[col * num_rows + idx] - max
if I_opid == 0:
curr = rnnt_helper.exponential(curr)
else:
curr = rnnt_helper.identity(curr)
idx += CTA_REDUCE_SIZE
while idx < num_rows:
activation_ = acts[col * num_rows + idx] - max
if I_opid == 0 and R_opid == 0:
curr = rnnt_helper.add(curr, rnnt_helper.exponential(activation_))
elif I_opid == 0 and R_opid == 1:
curr = rnnt_helper.maximum(curr, rnnt_helper.exponential(activation_))
elif I_opid == 1 and R_opid == 0:
curr = rnnt_helper.add(curr, rnnt_helper.identity(activation_))
else:
curr = rnnt_helper.maximum(curr, rnnt_helper.identity(activation_))
idx += CTA_REDUCE_SIZE
# // Sum thread-totals over the CTA.
curr = CTAReduce(tid, curr, storage, num_rows, R_opid)
# // Store result in out (inplace, I_op: exponential)
if tid == 0:
output[col] = -max - math.log(curr)
def ReduceHelper(
I_opid: int,
R_opid: int,
acts: torch.Tensor,
output: torch.Tensor,
num_rows: int,
num_cols: int,
minus: bool,
stream,
):
"""
CUDA Warp reduction kernel helper which reduces via the R_Op.Add and writes
the result to `output` according to I_op id.
The result is stored in the blockIdx.
Note:
Efficient warp occurs at input shapes of 2 ^ K.
References:
- Warp Primitives [https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/]
Args:
I_opid: Operator ID for input. See I_Op for more information.
R_opid: Operator ID for reduction. See R_Op for more information.
acts: Flatened activation matrix of shape [B * T * U * (V+1)].
output: Flatened output matrix of shape [B * T * U * (V+1)]. Data will be overwritten.
num_rows: Vocabulary size (including blank token) - V+1.
Represents the number of threads per block.
num_cols: Flattened shape of activation matrix, without vocabulary dimension (B * T * U).
Represents number of blocks per grid.
minus: Bool flag whether to add or subtract as reduction.
If minus is set; calls _reduce_minus, else calls _reduce_rows kernel.
stream: CUDA Stream.
"""
if minus:
grid_size = num_cols
# call kernel
_reduce_minus[grid_size, CTA_REDUCE_SIZE, stream, 0](I_opid, R_opid, acts, output, num_rows)
else:
grid_size = num_cols
# call kernel
_reduce_rows[grid_size, CTA_REDUCE_SIZE, stream, 0](I_opid, R_opid, acts, output, num_rows)
return True
def reduce_exp(acts: torch.Tensor, denom, rows: int, cols: int, minus: bool, stream):
"""
Helper method to call the Warp Reduction Kernel to perform `exp` reduction.
Note:
Efficient warp occurs at input shapes of 2 ^ K.
References:
- Warp Primitives [https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/]
Args:
acts: Flatened activation matrix of shape [B * T * U * (V+1)].
output: Flatened output matrix of shape [B * T * U * (V+1)]. Data will be overwritten.
rows: Vocabulary size (including blank token) - V+1.
Represents the number of threads per block.
cols: Flattened shape of activation matrix, without vocabulary dimension (B * T * U).
Represents number of blocks per grid.
minus: Bool flag whether to add or subtract as reduction.
If minus is set; calls _reduce_minus, else calls _reduce_rows kernel.
stream: CUDA Stream.
"""
return ReduceHelper(
I_opid=I_Op.EXPONENTIAL.value,
R_opid=R_Op.ADD.value,
acts=acts,
output=denom,
num_rows=rows,
num_cols=cols,
minus=minus,
stream=stream,
)
def reduce_max(acts: torch.Tensor, denom, rows: int, cols: int, minus: bool, stream):
"""
Helper method to call the Warp Reduction Kernel to perform `max` reduction.
Note:
Efficient warp occurs at input shapes of 2 ^ K.
References:
- Warp Primitives [https://developer.nvidia.com/blog/using-cuda-warp-level-primitives/]
Args:
acts: Flatened activation matrix of shape [B * T * U * (V+1)].
output: Flatened output matrix of shape [B * T * U * (V+1)]. Data will be overwritten.
rows: Vocabulary size (including blank token) - V+1.
Represents the number of threads per block.
cols: Flattened shape of activation matrix, without vocabulary dimension (B * T * U).
Represents number of blocks per grid.
minus: Bool flag whether to add or subtract as reduction.
If minus is set; calls _reduce_minus, else calls _reduce_rows kernel.
stream: CUDA Stream.
"""
return ReduceHelper(
I_opid=I_Op.IDENTITY.value,
R_opid=R_Op.MAXIMUM.value,
acts=acts,
output=denom,
num_rows=rows,
num_cols=cols,
minus=minus,
stream=stream,
)
@@ -0,0 +1,68 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 enum
import numpy as np
from numba import float32
# Internal globals
_THREADS_PER_BLOCK = 32
_WARP_SIZE = 32
_DTYPE = float32
# Constants
FP32_INF = np.inf
FP32_NEG_INF = -np.inf
THRESHOLD = 1e-1
"""
Getters
"""
def threads_per_block():
global _THREADS_PER_BLOCK
return _THREADS_PER_BLOCK
def warp_size():
global _WARP_SIZE
return _WARP_SIZE
def dtype():
global _DTYPE
return _DTYPE
# RNNT STATUS
class RNNTStatus(enum.Enum):
RNNT_STATUS_SUCCESS = 0
RNNT_STATUS_INVALID_VALUE = 1
@@ -0,0 +1,129 @@
# Copyright (c) 2021, 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.
#
# Copyright 2018-2019, Mingkun Huang
#
# 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 math
from typing import Optional
import torch
from numba import cuda
from nemo.collections.asr.parts.numba.rnnt_loss.utils import global_constants
threshold = global_constants.THRESHOLD
@cuda.jit(device=True, inline=True)
def log_sum_exp(a: float, b: float):
if a == global_constants.FP32_NEG_INF:
return b
if b == global_constants.FP32_NEG_INF:
return a
if a > b:
return math.log1p(math.exp(b - a)) + a
else:
return math.log1p(math.exp(a - b)) + b
@cuda.jit(device=True, inline=True)
def div_up(x: int, y: int):
return (x + y - 1) // y
@cuda.jit(device=True)
def maximum(x, y):
if x < y:
return y
else:
return x
@cuda.jit(device=True)
def add(x, y):
return x + y
@cuda.jit(device=True)
def identity(x):
return x
@cuda.jit(device=True)
def negate(x):
return -x
@cuda.jit(device=True)
def exponential(x):
return math.exp(x)
@cuda.jit(device=True)
def log_plus(p1: float, p2: float):
if p1 == global_constants.FP32_NEG_INF:
return p2
if p2 == global_constants.FP32_NEG_INF:
return p1
result = math.log1p(math.exp(-math.fabs(p1 - p2))) + maximum(p1, p2)
return result
def get_workspace_size(
maxT: int, maxU: int, minibatch: int, gpu: bool
) -> (Optional[int], global_constants.RNNTStatus):
if minibatch <= 0 or maxT <= 0 or maxU <= 0:
return (None, global_constants.RNNTStatus.RNNT_STATUS_INVALID_VALUE)
# per minibatch memory
per_minibatch_size = 0
# alphas & betas
per_minibatch_size += maxT * maxU * 2
if not gpu:
# // blank & label log probability cache
per_minibatch_size += maxT * maxU * 2
else:
# // softmax denominator
per_minibatch_size += maxT * maxU
# // forward - backward loglikelihood
per_minibatch_size += 2
size = per_minibatch_size * minibatch
return (size, global_constants.RNNTStatus.RNNT_STATUS_SUCCESS)
def flatten_tensor(x: torch.Tensor):
original_shape = x.shape
x = x.view([-1])
return x, original_shape
@@ -0,0 +1,18 @@
# Copyright (c) 2021, 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.
from nemo.collections.asr.parts.numba.spec_augment.spec_aug_numba import (
SpecAugmentNumba,
spec_augment_launch_heuristics,
)
@@ -0,0 +1,290 @@
# Copyright (c) 2021, 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 torch
import torch.nn as nn
from numba import cuda
from nemo.core.classes import Typing, typecheck
from nemo.core.neural_types import LengthsType, NeuralType, SpectrogramType
from nemo.utils import logging
MAX_THREAD_BUFFER = 512
@cuda.jit()
def spec_augment_kernel(
x: torch.Tensor,
x_len: torch.Tensor,
freq_starts: torch.Tensor,
freq_widths: torch.Tensor,
time_starts: torch.Tensor,
time_widths: torch.Tensor,
mask_value: float,
):
"""
Numba CUDA kernel to perform SpecAugment in-place on the GPU.
Parallelize over freq and time axis, parallel threads over batch.
Sequential over masks (adaptive in time).
Args:
x: Pytorch tensor of shape [B, F, T] with the acoustic features.
x_len: Pytorch tensor of shape [B] with the lengths of the padded sequence.
freq_starts: Pytorch tensor of shape [B, M_f] with the start indices of freq masks.
freq_widths: Pytorch tensor of shape [B, M_f] with the width of freq masks.
time_starts: Pytorch tensor of shape [B, M_t] with the start indices of time masks.
time_widths: Pytorch tensor of shape [B, M_t] with the width of time masks.
mask_value: Float value that will be used as mask value.
"""
f = cuda.blockIdx.x # indexes the Freq dim
t = cuda.blockIdx.y # indexes the Time dim
tid = cuda.threadIdx.x # index of the current mask
threads_per_block = cuda.blockDim.x
# Compute the number of masks over freq axis
len_f = freq_starts.shape[1]
# For all samples in the batch, apply the freq mask
for bidx in range(0, x.shape[0], threads_per_block):
# Resolve the index of the batch (case where more masks than MAX_THREAD_BUFFER)
bm_idx = bidx * threads_per_block + tid
# For `len_f` number of freq masks that must be applied
for fidx in range(0, len_f):
# If resolved freq mask index < total number of freq masks
if fidx < len_f:
# Access the start index and width of this freq mask
f_start = freq_starts[bm_idx, fidx]
f_width = freq_widths[bm_idx, fidx]
# If block idx `f` >= start and < (start + width) of this freq mask
if f >= f_start and f < (f_start + f_width):
x[bm_idx, f, t] = mask_value
# Compute the number of masks over time axis
len_t = time_starts.shape[1]
# For all samples in the batch, apply the time mask
for b_idx in range(0, x.shape[0], threads_per_block):
# Resolve the index of the batch (case where more masks than MAX_THREAD_BUFFER)
bm_idx = bidx * threads_per_block + tid
# For `len_t` number of freq masks that must be applied
for tidx in range(0, len_t):
# If resolved time mask index < total number of time masks
if tidx < len_t:
# Access the start index and width of this time mask
t_start = time_starts[bm_idx, tidx]
t_width = time_widths[bm_idx, tidx]
# If block idx `t` >= start and < (start + width) of this time mask
if t >= t_start and t < (t_start + t_width):
# Current block idx `t` < current seq length x_len[b]
# This ensure that we mask only upto the length of that sample
# Everything after that index is padded value so unnecessary to mask
if t < x_len[bm_idx]:
x[bm_idx, f, t] = mask_value
def spec_augment_launch_heuristics(x: torch.Tensor, length: torch.Tensor):
"""
Heuristics to determins whether pytorch implementation or numba implementation is selected.
Assumes numba cuda is supported.
Args:
x: Torch tensor of shape [B, F, T]
length: Optional, Torch of tensor of shape [B] - containing lengths of the tensor.
Returns:
True if numba kernel should be selected, else False
"""
if not x.is_cuda:
return False
if length is None:
return False
if x.shape[0] < 8:
return False
return True
def launch_spec_augment_kernel(
x: torch.Tensor,
x_len: torch.Tensor,
freq_starts: torch.Tensor,
freq_lengths: torch.Tensor,
time_starts: torch.Tensor,
time_lengths: torch.Tensor,
freq_masks: int,
time_masks: int,
mask_value: float,
):
"""
Helper method to launch the SpecAugment kernel
Args:
x: Pytorch tensor of shape [B, F, T] with the acoustic features.
x_len: Pytorch tensor of shape [B] with the lengths of the padded sequence.
freq_starts: Pytorch tensor of shape [B, M_f] with the start indices of freq masks.
freq_widths: Pytorch tensor of shape [B, M_f] with the width of freq masks.
time_starts: Pytorch tensor of shape [B, M_t] with the start indices of time masks.
time_widths: Pytorch tensor of shape [B, M_t] with the width of time masks.
freq_masks: Int value that determines the number of time masks.
time_masks: Int value that determines the number of freq masks.
mask_value: Float value that will be used as mask value.
Returns:
The spec augmented tensor 'x'
"""
# Setup CUDA stream
sh = x.shape
stream = cuda.external_stream(torch.cuda.current_stream(x.device).cuda_stream)
if time_masks > 0 or freq_masks > 0:
# Parallelize over freq and time axis, parallel threads over batch
# Sequential over masks (adaptive in time).
blocks_per_grid = [sh[1], sh[2]]
# threads_per_block = min(MAX_THREAD_BUFFER, max(freq_masks, time_masks))
threads_per_block = min(MAX_THREAD_BUFFER, x.shape[0])
# Numba does not support fp16, force cast to fp32 temporarily at the expense of memory
original_dtype = x.dtype
cast_x = False
if x.dtype == torch.float16:
x = x.float()
cast_x = True
# Launch CUDA kernel
spec_augment_kernel[blocks_per_grid, threads_per_block, stream, 0](
x, x_len, freq_starts, freq_lengths, time_starts, time_lengths, mask_value
)
torch.cuda.synchronize()
# Recast back to original dtype if earlier cast was performed
if cast_x:
x = x.to(dtype=original_dtype)
return x
class SpecAugmentNumba(nn.Module, Typing):
"""
Zeroes out(cuts) random continuous horisontal or
vertical segments of the spectrogram as described in
SpecAugment (https://arxiv.org/abs/1904.08779).
Utilizes a Numba CUDA kernel to perform inplace edit of the input without loops.
Parallelize over freq and time axis, parallel threads over batch.
Sequential over masks (adaptive in time).
Args:
freq_masks - how many frequency segments should be cut
time_masks - how many time segments should be cut
freq_width - maximum number of frequencies to be cut in one segment
time_width - maximum number of time steps to be cut in one segment.
Can be a positive integer or a float value in the range [0, 1].
If positive integer value, defines maximum number of time steps
to be cut in one segment.
If a float value, defines maximum percentage of timesteps that
are cut adaptively.
rng: Ignored.
"""
@property
def input_types(self):
"""Returns definitions of module input types
"""
return {
"input_spec": NeuralType(('B', 'D', 'T'), SpectrogramType()),
"length": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output types
"""
return {"augmented_spec": NeuralType(('B', 'D', 'T'), SpectrogramType())}
def __init__(
self, freq_masks=0, time_masks=0, freq_width=10, time_width=0.1, rng=None, mask_value=0.0,
):
super().__init__()
# Message to mention that numba specaugment kernel will be available
# if input device is CUDA and lengths are provided
logging.debug("Numba SpecAugment kernel is available")
self.freq_masks = freq_masks
self.time_masks = time_masks
self.freq_width = freq_width
self.time_width = time_width
self.mask_value = mask_value
# Unused
self.rng = rng
if self.rng is not None:
logging.warning("`rng` was supplied to SpecAugmentNumba, but it is not used.")
if isinstance(time_width, int):
self.adaptive_temporal_width = False
else:
if time_width > 1.0 or time_width < 0.0:
raise ValueError('If `time_width` is a float value, must be in range [0, 1]')
self.adaptive_temporal_width = True
@typecheck()
@torch.no_grad()
def forward(self, input_spec, length):
sh = input_spec.shape
bs = sh[0]
if self.adaptive_temporal_width:
time_width = max(1, int(sh[2] * self.time_width))
else:
time_width = self.time_width
# Construct the freq and time masks as well as start positions
if self.freq_masks > 0:
freq_starts = torch.randint(
0, sh[1] - self.freq_width + 1, size=[bs, self.freq_masks], device=input_spec.device
)
freq_lengths = torch.randint(0, self.freq_width + 1, size=[bs, self.freq_masks], device=input_spec.device)
else:
freq_starts = torch.zeros([bs, 1], dtype=torch.int64, device=input_spec.device)
freq_lengths = torch.zeros([bs, 1], dtype=torch.int64, device=input_spec.device)
if self.time_masks > 0:
time_starts = torch.randint(
0, sh[2] - time_width + 1, size=[bs, self.time_masks], device=input_spec.device
)
time_lengths = torch.randint(0, time_width + 1, size=[bs, self.time_masks], device=input_spec.device)
else:
time_starts = torch.zeros([bs, 1], dtype=torch.int64, device=input_spec.device)
time_lengths = torch.zeros([bs, 1], dtype=torch.int64, device=input_spec.device)
x = launch_spec_augment_kernel(
input_spec,
length,
freq_starts=freq_starts,
freq_lengths=freq_lengths,
time_starts=time_starts,
time_lengths=time_lengths,
freq_masks=self.freq_masks,
time_masks=self.time_masks,
mask_value=self.mask_value,
)
return x
@@ -0,0 +1,41 @@
# Copyright (c) 2021, 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.
from nemo.collections.asr.parts.preprocessing.feature_loader import ExternalFeatureLoader
from nemo.collections.asr.parts.preprocessing.features import (
STFT,
FeaturizerFactory,
FilterbankFeatures,
STFTExactPad,
STFTPatch,
WaveformFeaturizer,
)
from nemo.collections.asr.parts.preprocessing.perturb import (
AudioAugmentor,
AugmentationDataset,
GainPerturbation,
ImpulsePerturbation,
NoisePerturbation,
Perturbation,
RirAndNoisePerturbation,
ShiftPerturbation,
SpeedPerturbation,
TimeStretchPerturbation,
TranscodePerturbation,
WhiteNoisePerturbation,
perturbation_types,
process_augmentations,
register_perturbation,
)
from nemo.collections.asr.parts.preprocessing.segment import AudioSegment
@@ -0,0 +1,69 @@
# Copyright (c) 2020, 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 numpy as np
import torch
class ExternalFeatureLoader(object):
"""Feature loader that load external features store in certain format.
Currently support pickle, npy and npz format.
"""
def __init__(self, file_path, sample_rate=16000, augmentor=None):
"""Load feature samples from file_path.
file_path (str) is the path of the file that stores feature/sample.
"""
self._file_path = file_path
self.augmentor = augmentor
def load_feature_from_file(self, file_path):
"""Load samples from file_path and convert it to be of type float32
file_path (str) is the path of the file that stores feature/sample.
"""
# load pickle/npy/npz file
try:
samples = np.load(file_path, allow_pickle=True)
return self._convert_samples_to_float32(samples)
except:
raise TypeError(
"Error open feature files. Probably not supported the format yet. Support pkl, npz, and npy format now. "
)
# TODO load other type of files such as kaldi io ark
@staticmethod
def _convert_samples_to_float32(samples):
"""Convert sample type to float32.
Integers will be scaled to [-1, 1] in float32.
"""
float32_samples = samples.astype('float32')
if samples.dtype in np.sctypes['int']:
bits = np.iinfo(samples.dtype).bits
float32_samples *= 1.0 / 2 ** (bits - 1)
elif samples.dtype in np.sctypes['float']:
pass
else:
raise TypeError("Unsupported sample type: %s." % samples.dtype)
return float32_samples
def process(self, file_path, offset=0, duration=0, trim=False, orig_sr=None):
feature = self.load_feature_from_file(file_path)
return self.process_segment(feature)
def process_segment(self, feature_segment):
if self.augmentor:
# augmentor for external features. Here possible augmentor for external embedding feature is Diaconis Augmentation and might be implemented later
self.augmentor.perturb(feature_segment)
return torch.tensor(feature_segment, dtype=torch.float)
return torch.tensor(feature_segment, dtype=torch.float)
@@ -0,0 +1,432 @@
# Copyright (c) 2020, 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.
#
# Copyright (c) 2018 Ryan Leary
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This file contains code artifacts adapted from https://github.com/ryanleary/patter
import math
import librosa
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from librosa.util import tiny
from torch.autograd import Variable
from torch_stft import STFT
from nemo.collections.asr.parts.preprocessing.perturb import AudioAugmentor
from nemo.collections.asr.parts.preprocessing.segment import AudioSegment
from nemo.collections.common.parts.patch_utils import stft_patch
from nemo.utils import logging
CONSTANT = 1e-5
def normalize_batch(x, seq_len, normalize_type):
if normalize_type == "per_feature":
x_mean = torch.zeros((seq_len.shape[0], x.shape[1]), dtype=x.dtype, device=x.device)
x_std = torch.zeros((seq_len.shape[0], x.shape[1]), dtype=x.dtype, device=x.device)
for i in range(x.shape[0]):
if x[i, :, : seq_len[i]].shape[1] == 1:
raise ValueError(
"normalize_batch with `per_feature` normalize_type received a tensor of length 1. This will result "
"in torch.std() returning nan"
)
x_mean[i, :] = x[i, :, : seq_len[i]].mean(dim=1)
x_std[i, :] = x[i, :, : seq_len[i]].std(dim=1)
# make sure x_std is not zero
x_std += CONSTANT
return (x - x_mean.unsqueeze(2)) / x_std.unsqueeze(2)
elif normalize_type == "all_features":
x_mean = torch.zeros(seq_len.shape, dtype=x.dtype, device=x.device)
x_std = torch.zeros(seq_len.shape, dtype=x.dtype, device=x.device)
for i in range(x.shape[0]):
x_mean[i] = x[i, :, : seq_len[i].item()].mean()
x_std[i] = x[i, :, : seq_len[i].item()].std()
# make sure x_std is not zero
x_std += CONSTANT
return (x - x_mean.view(-1, 1, 1)) / x_std.view(-1, 1, 1)
elif "fixed_mean" in normalize_type and "fixed_std" in normalize_type:
x_mean = torch.tensor(normalize_type["fixed_mean"], device=x.device)
x_std = torch.tensor(normalize_type["fixed_std"], device=x.device)
return (x - x_mean.view(x.shape[0], x.shape[1]).unsqueeze(2)) / x_std.view(x.shape[0], x.shape[1]).unsqueeze(2)
else:
return x
def splice_frames(x, frame_splicing):
""" Stacks frames together across feature dim
input is batch_size, feature_dim, num_frames
output is batch_size, feature_dim*frame_splicing, num_frames
"""
seq = [x]
for n in range(1, frame_splicing):
seq.append(torch.cat([x[:, :, :n], x[:, :, n:]], dim=2))
return torch.cat(seq, dim=1)
class WaveformFeaturizer(object):
def __init__(self, sample_rate=16000, int_values=False, augmentor=None):
self.augmentor = augmentor if augmentor is not None else AudioAugmentor()
self.sample_rate = sample_rate
self.int_values = int_values
def max_augmentation_length(self, length):
return self.augmentor.max_augmentation_length(length)
def process(self, file_path, offset=0, duration=0, trim=False, orig_sr=None):
audio = AudioSegment.from_file(
file_path,
target_sr=self.sample_rate,
int_values=self.int_values,
offset=offset,
duration=duration,
trim=trim,
orig_sr=orig_sr,
)
return self.process_segment(audio)
def process_segment(self, audio_segment):
self.augmentor.perturb(audio_segment)
return torch.tensor(audio_segment.samples, dtype=torch.float)
@classmethod
def from_config(cls, input_config, perturbation_configs=None):
if perturbation_configs is not None:
aa = AudioAugmentor.from_config(perturbation_configs)
else:
aa = None
sample_rate = input_config.get("sample_rate", 16000)
int_values = input_config.get("int_values", False)
return cls(sample_rate=sample_rate, int_values=int_values, augmentor=aa)
class FeaturizerFactory(object):
def __init__(self):
pass
@classmethod
def from_config(cls, input_cfg, perturbation_configs=None):
return WaveformFeaturizer.from_config(input_cfg, perturbation_configs=perturbation_configs)
# Create helper class to patch forward func for use with AMP
class STFTPatch(STFT):
def forward(self, input_data):
return super().transform(input_data)[0]
# Create helper class for STFT that yields num_frames = num_samples // hop_length
class STFTExactPad(STFTPatch):
"""adapted from Prem Seetharaman's https://github.com/pseeth/pytorch-stft"""
def __init__(self, *params, **kw_params):
super().__init__(*params, **kw_params)
self.pad_amount = (self.filter_length - self.hop_length) // 2
def inverse(self, magnitude, phase):
recombine_magnitude_phase = torch.cat([magnitude * torch.cos(phase), magnitude * torch.sin(phase)], dim=1)
inverse_transform = F.conv_transpose1d(
recombine_magnitude_phase,
Variable(self.inverse_basis, requires_grad=False),
stride=self.hop_length,
padding=0,
)
if self.window is not None:
window_sum = librosa.filters.window_sumsquare(
self.window,
magnitude.size(-1),
hop_length=self.hop_length,
win_length=self.win_length,
n_fft=self.filter_length,
dtype=np.float32,
)
# remove modulation effects
approx_nonzero_indices = torch.from_numpy(np.where(window_sum > tiny(window_sum))[0])
window_sum = torch.autograd.Variable(torch.from_numpy(window_sum), requires_grad=False).to(
magnitude.device
)
inverse_transform[..., approx_nonzero_indices] /= window_sum[approx_nonzero_indices]
# scale by hop ratio
inverse_transform *= self.filter_length / self.hop_length
inverse_transform = inverse_transform[..., self.pad_amount :]
inverse_transform = inverse_transform[..., : -self.pad_amount :]
inverse_transform = inverse_transform.squeeze(1)
return inverse_transform
class FilterbankFeatures(nn.Module):
"""Featurizer that converts wavs to Mel Spectrograms.
See AudioToMelSpectrogramPreprocessor for args.
"""
def __init__(
self,
sample_rate=16000,
n_window_size=320,
n_window_stride=160,
window="hann",
normalize="per_feature",
n_fft=None,
preemph=0.97,
nfilt=64,
lowfreq=0,
highfreq=None,
log=True,
log_zero_guard_type="add",
log_zero_guard_value=2 ** -24,
dither=CONSTANT,
pad_to=16,
max_duration=16.7,
frame_splicing=1,
exact_pad=False,
stft_exact_pad=False, # TODO: Remove this in 1.1.0
stft_conv=False, # TODO: Remove this in 1.1.0
pad_value=0,
mag_power=2.0,
use_grads=False,
):
super().__init__()
if stft_conv or stft_exact_pad:
logging.warning(
"Using torch_stft is deprecated and will be removed in 1.1.0. Please set stft_conv and stft_exact_pad "
"to False for FilterbankFeatures and AudioToMelSpectrogramPreprocessor. Please set exact_pad to True "
"as needed."
)
if (exact_pad or stft_exact_pad) and n_window_stride % 2 == 1:
raise NotImplementedError(
f"{self} received exact_pad == True, but hop_size was odd. If audio_length % hop_size == 0. Then the "
"returned spectrogram would not be of length audio_length // hop_size. Please use an even hop_size."
)
self.log_zero_guard_value = log_zero_guard_value
if (
n_window_size is None
or n_window_stride is None
or not isinstance(n_window_size, int)
or not isinstance(n_window_stride, int)
or n_window_size <= 0
or n_window_stride <= 0
):
raise ValueError(
f"{self} got an invalid value for either n_window_size or "
f"n_window_stride. Both must be positive ints."
)
logging.info(f"PADDING: {pad_to}")
self.win_length = n_window_size
self.hop_length = n_window_stride
self.n_fft = n_fft or 2 ** math.ceil(math.log2(self.win_length))
self.stft_pad_amount = (self.n_fft - self.hop_length) // 2 if exact_pad else None
self.stft_exact_pad = stft_exact_pad
self.stft_conv = stft_conv
if stft_conv:
logging.info("STFT using conv")
if stft_exact_pad:
logging.info("STFT using exact pad")
self.stft = STFTExactPad(self.n_fft, self.hop_length, self.win_length, window)
else:
self.stft = STFTPatch(self.n_fft, self.hop_length, self.win_length, window)
else:
logging.info("STFT using torch")
if exact_pad:
logging.info("STFT using exact pad")
torch_windows = {
'hann': torch.hann_window,
'hamming': torch.hamming_window,
'blackman': torch.blackman_window,
'bartlett': torch.bartlett_window,
'none': None,
}
window_fn = torch_windows.get(window, None)
window_tensor = window_fn(self.win_length, periodic=False) if window_fn else None
self.register_buffer("window", window_tensor)
self.stft = lambda x: stft_patch(
x,
n_fft=self.n_fft,
hop_length=self.hop_length,
win_length=self.win_length,
center=False if exact_pad else True,
window=self.window.to(dtype=torch.float),
return_complex=False,
)
self.normalize = normalize
self.log = log
self.dither = dither
self.frame_splicing = frame_splicing
self.nfilt = nfilt
self.preemph = preemph
self.pad_to = pad_to
highfreq = highfreq or sample_rate / 2
filterbanks = torch.tensor(
librosa.filters.mel(sample_rate, self.n_fft, n_mels=nfilt, fmin=lowfreq, fmax=highfreq), dtype=torch.float
).unsqueeze(0)
self.register_buffer("fb", filterbanks)
# Calculate maximum sequence length
max_length = self.get_seq_len(torch.tensor(max_duration * sample_rate, dtype=torch.float))
max_pad = pad_to - (max_length % pad_to) if pad_to > 0 else 0
self.max_length = max_length + max_pad
self.pad_value = pad_value
self.mag_power = mag_power
# We want to avoid taking the log of zero
# There are two options: either adding or clamping to a small value
if log_zero_guard_type not in ["add", "clamp"]:
raise ValueError(
f"{self} received {log_zero_guard_type} for the "
f"log_zero_guard_type parameter. It must be either 'add' or "
f"'clamp'."
)
self.use_grads = use_grads
if not use_grads:
self.forward = torch.no_grad()(self.forward)
# log_zero_guard_value is the the small we want to use, we support
# an actual number, or "tiny", or "eps"
self.log_zero_guard_type = log_zero_guard_type
logging.debug(f"sr: {sample_rate}")
logging.debug(f"n_fft: {self.n_fft}")
logging.debug(f"win_length: {self.win_length}")
logging.debug(f"hop_length: {self.hop_length}")
logging.debug(f"n_mels: {nfilt}")
logging.debug(f"fmin: {lowfreq}")
logging.debug(f"fmax: {highfreq}")
logging.debug(f"using grads: {use_grads}")
def log_zero_guard_value_fn(self, x):
if isinstance(self.log_zero_guard_value, str):
if self.log_zero_guard_value == "tiny":
return torch.finfo(x.dtype).tiny
elif self.log_zero_guard_value == "eps":
return torch.finfo(x.dtype).eps
else:
raise ValueError(
f"{self} received {self.log_zero_guard_value} for the "
f"log_zero_guard_type parameter. It must be either a "
f"number, 'tiny', or 'eps'"
)
else:
return self.log_zero_guard_value
def get_seq_len(self, seq_len):
if isinstance(self.stft, STFT):
pad_amount = self.stft.pad_amount * 2
else:
# Assuming that center is True is stft_pad_amount = 0
pad_amount = self.stft_pad_amount * 2 if self.stft_pad_amount is not None else self.n_fft // 2 * 2
seq_len = torch.floor((seq_len + pad_amount - self.n_fft) / self.hop_length) + 1
return seq_len.to(dtype=torch.long)
@property
def filter_banks(self):
return self.fb
def forward(self, x, seq_len):
seq_len = self.get_seq_len(seq_len.float())
if self.stft_pad_amount is not None:
x = torch.nn.functional.pad(
x.unsqueeze(1), (self.stft_pad_amount, self.stft_pad_amount), "reflect"
).squeeze(1)
# dither (only in training mode for eval determinism)
if self.training and self.dither > 0:
x += self.dither * torch.randn_like(x)
# do preemphasis
if self.preemph is not None:
x = torch.cat((x[:, 0].unsqueeze(1), x[:, 1:] - self.preemph * x[:, :-1]), dim=1)
# disable autocast to get full range of stft values
with torch.cuda.amp.autocast(enabled=False):
x = self.stft(x)
# torch returns real, imag; so convert to magnitude
if not self.stft_conv:
# guard is needed for sqrt if grads are passed through
guard = 0 if not self.use_grads else CONSTANT
if x.dtype in [torch.cfloat, torch.cdouble]:
x = torch.view_as_real(x)
x = torch.sqrt(x.pow(2).sum(-1) + guard)
# get power spectrum
if self.mag_power != 1.0:
x = x.pow(self.mag_power)
# dot with filterbank energies
x = torch.matmul(self.fb.to(x.dtype), x)
# log features if required
if self.log:
if self.log_zero_guard_type == "add":
x = torch.log(x + self.log_zero_guard_value_fn(x))
elif self.log_zero_guard_type == "clamp":
x = torch.log(torch.clamp(x, min=self.log_zero_guard_value_fn(x)))
else:
raise ValueError("log_zero_guard_type was not understood")
# frame splicing if required
if self.frame_splicing > 1:
x = splice_frames(x, self.frame_splicing)
# normalize if required
if self.normalize:
x = normalize_batch(x, seq_len, normalize_type=self.normalize)
# mask to zero any values beyond seq_len in batch, pad to multiple of `pad_to` (for efficiency)
max_len = x.size(-1)
mask = torch.arange(max_len).to(x.device)
mask = mask.expand(x.size(0), max_len) >= seq_len.unsqueeze(1)
x = x.masked_fill(mask.unsqueeze(1).type(torch.bool).to(device=x.device), self.pad_value)
del mask
pad_to = self.pad_to
if pad_to == "max":
x = nn.functional.pad(x, (0, self.max_length - x.size(-1)), value=self.pad_value)
elif pad_to > 0:
pad_amt = x.size(-1) % pad_to
if pad_amt != 0:
x = nn.functional.pad(x, (0, pad_to - pad_amt), value=self.pad_value)
return x, seq_len
@@ -0,0 +1,895 @@
# Copyright (c) 2020, 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.
#
# Copyright (c) 2018 Ryan Leary
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This file contains code artifacts adapted from https://github.com/ryanleary/patter
import copy
import io
import os
import random
import subprocess
from tempfile import NamedTemporaryFile
from typing import List, Optional, Union
import librosa
import numpy as np
import soundfile as sf
import webdataset as wd
from omegaconf import DictConfig, OmegaConf
from scipy import signal
from torch.utils.data import IterableDataset
from nemo.collections.asr.parts.preprocessing.segment import AudioSegment
from nemo.collections.common.parts.preprocessing import collections, parsers
from nemo.utils import logging
try:
from nemo.collections.asr.parts.utils import numba_utils
HAVE_NUMBA = True
except (ImportError, ModuleNotFoundError):
HAVE_NUMBA = False
def read_one_audiosegment(manifest, target_sr, rng, tarred_audio=False, audio_dataset=None):
if tarred_audio:
if audio_dataset is None:
raise TypeError("Expected augmentation dataset but got None")
audio_file, file_id = next(audio_dataset)
manifest_idx = manifest.mapping[file_id]
manifest_entry = manifest[manifest_idx]
offset = 0 if manifest_entry.offset is None else manifest_entry.offset
duration = 0 if manifest_entry.duration is None else manifest_entry.duration
else:
audio_record = rng.sample(manifest.data, 1)[0]
audio_file = audio_record.audio_file
offset = 0 if audio_record.offset is None else audio_record.offset
duration = 0 if audio_record.duration is None else audio_record.duration
return AudioSegment.from_file(audio_file, target_sr=target_sr, offset=offset, duration=duration)
class Perturbation(object):
def max_augmentation_length(self, length):
return length
def perturb(self, data):
raise NotImplementedError
class SpeedPerturbation(Perturbation):
"""
Performs Speed Augmentation by re-sampling the data to a different sampling rate,
which does not preserve pitch.
Note: This is a very slow operation for online augmentation. If space allows,
it is preferable to pre-compute and save the files to augment the dataset.
Args:
sr: Original sampling rate.
resample_type: Type of resampling operation that will be performed.
For better speed using `resampy`'s fast resampling method, use `resample_type='kaiser_fast'`.
For high-quality resampling, set `resample_type='kaiser_best'`.
To use `scipy.signal.resample`, set `resample_type='fft'` or `resample_type='scipy'`
min_speed_rate: Minimum sampling rate modifier.
max_speed_rate: Maximum sampling rate modifier.
num_rates: Number of discrete rates to allow. Can be a positive or negative
integer.
If a positive integer greater than 0 is provided, the range of
speed rates will be discretized into `num_rates` values.
If a negative integer or 0 is provided, the full range of speed rates
will be sampled uniformly.
Note: If a positive integer is provided and the resultant discretized
range of rates contains the value '1.0', then those samples with rate=1.0,
will not be augmented at all and simply skipped. This is to unnecessary
augmentation and increase computation time. Effective augmentation chance
in such a case is = `prob * (num_rates - 1 / num_rates) * 100`% chance
where `prob` is the global probability of a sample being augmented.
rng: Random seed number.
"""
def __init__(self, sr, resample_type, min_speed_rate=0.9, max_speed_rate=1.1, num_rates=5, rng=None):
min_rate = min(min_speed_rate, max_speed_rate)
if min_rate < 0.0:
raise ValueError("Minimum sampling rate modifier must be > 0.")
if resample_type not in ('kaiser_best', 'kaiser_fast', 'fft', 'scipy'):
raise ValueError("Supported `resample_type` values are ('kaiser_best', 'kaiser_fast', 'fft', 'scipy')")
self._sr = sr
self._min_rate = min_speed_rate
self._max_rate = max_speed_rate
self._num_rates = num_rates
if num_rates > 0:
self._rates = np.linspace(self._min_rate, self._max_rate, self._num_rates, endpoint=True)
self._res_type = resample_type
self._rng = random.Random() if rng is None else rng
def max_augmentation_length(self, length):
return length * self._max_rate
def perturb(self, data):
# Select speed rate either from choice or random sample
if self._num_rates < 0:
speed_rate = self._rng.uniform(self._min_rate, self._max_rate)
else:
speed_rate = self._rng.choice(self._rates)
# Skip perturbation in case of identity speed rate
if speed_rate == 1.0:
return
new_sr = int(self._sr * speed_rate)
data._samples = librosa.core.resample(data._samples, self._sr, new_sr, res_type=self._res_type)
class TimeStretchPerturbation(Perturbation):
"""
Time-stretch an audio series by a fixed rate while preserving pitch, based on [1, 2].
Note:
This is a simplified implementation, intended primarily for reference and pedagogical purposes.
It makes no attempt to handle transients, and is likely to produce audible artifacts.
Reference
[1] [Ellis, D. P. W. “A phase vocoder in Matlab.” Columbia University, 2002.]
(http://www.ee.columbia.edu/~dpwe/resources/matlab/pvoc/)
[2] [librosa.effects.time_stretch]
(https://librosa.github.io/librosa/generated/librosa.effects.time_stretch.html)
Args:
min_speed_rate: Minimum sampling rate modifier.
max_speed_rate: Maximum sampling rate modifier.
num_rates: Number of discrete rates to allow. Can be a positive or negative
integer.
If a positive integer greater than 0 is provided, the range of
speed rates will be discretized into `num_rates` values.
If a negative integer or 0 is provided, the full range of speed rates
will be sampled uniformly.
Note: If a positive integer is provided and the resultant discretized
range of rates contains the value '1.0', then those samples with rate=1.0,
will not be augmented at all and simply skipped. This is to avoid unnecessary
augmentation and increase computation time. Effective augmentation chance
in such a case is = `prob * (num_rates - 1 / num_rates) * 100`% chance
where `prob` is the global probability of a sample being augmented.
n_fft: Number of fft filters to be computed.
rng: Random seed number.
"""
def __init__(self, min_speed_rate=0.9, max_speed_rate=1.1, num_rates=5, n_fft=512, rng=None):
min_rate = min(min_speed_rate, max_speed_rate)
if min_rate < 0.0:
raise ValueError("Minimum sampling rate modifier must be > 0.")
self._min_rate = min_speed_rate
self._max_rate = max_speed_rate
self._num_rates = num_rates
if num_rates > 0:
self._rates = np.linspace(self._min_rate, self._max_rate, self._num_rates, endpoint=True)
self._rng = random.Random() if rng is None else rng
# Pre-compute constants
self._n_fft = int(n_fft)
self._hop_length = int(n_fft // 2)
# Pre-allocate buffers
self._phi_advance_fast = np.linspace(0, np.pi * self._hop_length, self._hop_length + 1)
self._scale_buffer_fast = np.empty(self._hop_length + 1, dtype=np.float32)
self._phi_advance_slow = np.linspace(0, np.pi * self._n_fft, self._n_fft + 1)
self._scale_buffer_slow = np.empty(self._n_fft + 1, dtype=np.float32)
def max_augmentation_length(self, length):
return length * self._max_rate
def perturb(self, data):
# Select speed rate either from choice or random sample
if self._num_rates < 0:
speed_rate = self._rng.uniform(self._min_rate, self._max_rate)
else:
speed_rate = self._rng.choice(self._rates)
# Skip perturbation in case of identity speed rate
if speed_rate == 1.0:
return
# Increase `n_fft` based on task (speed up or slow down audio)
# This greatly reduces upper bound of maximum time taken
# to compute slowed down audio segments.
if speed_rate >= 1.0: # Speed up audio
fft_multiplier = 1
phi_advance = self._phi_advance_fast
scale_buffer = self._scale_buffer_fast
else: # Slow down audio
fft_multiplier = 2
phi_advance = self._phi_advance_slow
scale_buffer = self._scale_buffer_slow
n_fft = int(self._n_fft * fft_multiplier)
hop_length = int(self._hop_length * fft_multiplier)
# Perform short-term Fourier transform (STFT)
stft = librosa.core.stft(data._samples, n_fft=n_fft, hop_length=hop_length)
# Stretch by phase vocoding
if HAVE_NUMBA:
stft_stretch = numba_utils.phase_vocoder(stft, speed_rate, phi_advance, scale_buffer)
else:
stft_stretch = librosa.core.phase_vocoder(stft, speed_rate, hop_length)
# Predict the length of y_stretch
len_stretch = int(round(len(data._samples) / speed_rate))
# Invert the STFT
y_stretch = librosa.core.istft(
stft_stretch, dtype=data._samples.dtype, hop_length=hop_length, length=len_stretch
)
data._samples = y_stretch
class GainPerturbation(Perturbation):
"""
Applies random gain to the audio.
Args:
min_gain_dbfs (float): Min gain level in dB
max_gain_dbfs (float): Max gain level in dB
rng: Random number generator
"""
def __init__(self, min_gain_dbfs=-10, max_gain_dbfs=10, rng=None):
self._min_gain_dbfs = min_gain_dbfs
self._max_gain_dbfs = max_gain_dbfs
self._rng = random.Random() if rng is None else rng
def perturb(self, data):
gain = self._rng.uniform(self._min_gain_dbfs, self._max_gain_dbfs)
# logging.debug("gain: %d", gain)
data._samples = data._samples * (10.0 ** (gain / 20.0))
class ImpulsePerturbation(Perturbation):
"""
Convolves audio with a Room Impulse Response.
Args:
manifest_path (list): Manifest file for RIRs
audio_tar_filepaths (list): Tar files, if RIR audio files are tarred
shuffle_n (int): Shuffle parameter for shuffling buffered files from the tar files
shift_impulse (bool): Shift impulse response to adjust for delay at the beginning
"""
def __init__(self, manifest_path=None, rng=None, audio_tar_filepaths=None, shuffle_n=128, shift_impulse=False):
self._manifest = collections.ASRAudioText(manifest_path, parser=parsers.make_parser([]), index_by_file_id=True)
self._audiodataset = None
self._tarred_audio = False
self._shift_impulse = shift_impulse
self._data_iterator = None
if audio_tar_filepaths:
self._tarred_audio = True
self._audiodataset = AugmentationDataset(manifest_path, audio_tar_filepaths, shuffle_n)
self._data_iterator = iter(self._audiodataset)
self._rng = random.Random() if rng is None else rng
def perturb(self, data):
impulse = read_one_audiosegment(
self._manifest,
data.sample_rate,
self._rng,
tarred_audio=self._tarred_audio,
audio_dataset=self._data_iterator,
)
if not self._shift_impulse:
impulse_norm = (impulse.samples - min(impulse.samples)) / (max(impulse.samples) - min(impulse.samples))
data._samples = signal.fftconvolve(data._samples, impulse_norm, "same")
else:
# Find peak and shift peak to left
impulse_norm = (impulse.samples - min(impulse.samples)) / (max(impulse.samples) - min(impulse.samples))
max_ind = np.argmax(np.abs(impulse_norm))
impulse_resp = impulse_norm[max_ind:]
delay_after = len(impulse_resp)
data._samples = signal.fftconvolve(data._samples, impulse_resp, "full")[:-delay_after]
class ShiftPerturbation(Perturbation):
"""
Perturbs audio by shifting the audio in time by a random amount between min_shift_ms and max_shift_ms.
The final length of the audio is kept unaltered by padding the audio with zeros.
Args:
min_shift_ms (float): Minimum time in milliseconds by which audio will be shifted
max_shift_ms (float): Maximum time in milliseconds by which audio will be shifted
rng: Random number generator
"""
def __init__(self, min_shift_ms=-5.0, max_shift_ms=5.0, rng=None):
self._min_shift_ms = min_shift_ms
self._max_shift_ms = max_shift_ms
self._rng = random.Random() if rng is None else rng
def perturb(self, data):
shift_ms = self._rng.uniform(self._min_shift_ms, self._max_shift_ms)
if abs(shift_ms) / 1000 > data.duration:
# TODO: do something smarter than just ignore this condition
return
shift_samples = int(shift_ms * data.sample_rate // 1000)
# logging.debug("shift: %s", shift_samples)
if shift_samples < 0:
data._samples[-shift_samples:] = data._samples[:shift_samples]
data._samples[:-shift_samples] = 0
elif shift_samples > 0:
data._samples[:-shift_samples] = data._samples[shift_samples:]
data._samples[-shift_samples:] = 0
class NoisePerturbation(Perturbation):
"""
Perturbation that adds noise to input audio.
Args:
manifest_path (str): Manifest file with paths to noise files
min_snr_db (float): Minimum SNR of audio after noise is added
max_snr_db (float): Maximum SNR of audio after noise is added
max_gain_db (float): Maximum gain that can be applied on the noise sample
audio_tar_filepaths (list) : Tar files, if noise audio files are tarred
shuffle_n (int): Shuffle parameter for shuffling buffered files from the tar files
orig_sr (int): Original sampling rate of the noise files
rng: Random number generator
"""
def __init__(
self,
manifest_path=None,
min_snr_db=10,
max_snr_db=50,
max_gain_db=300.0,
rng=None,
audio_tar_filepaths=None,
shuffle_n=100,
orig_sr=16000,
):
self._manifest = collections.ASRAudioText(manifest_path, parser=parsers.make_parser([]), index_by_file_id=True)
self._audiodataset = None
self._tarred_audio = False
self._orig_sr = orig_sr
self._data_iterator = None
if audio_tar_filepaths:
self._tarred_audio = True
self._audiodataset = AugmentationDataset(manifest_path, audio_tar_filepaths, shuffle_n)
self._data_iterator = iter(self._audiodataset)
self._rng = random.Random() if rng is None else rng
self._min_snr_db = min_snr_db
self._max_snr_db = max_snr_db
self._max_gain_db = max_gain_db
@property
def orig_sr(self):
return self._orig_sr
def get_one_noise_sample(self, target_sr):
return read_one_audiosegment(
self._manifest, target_sr, self._rng, tarred_audio=self._tarred_audio, audio_dataset=self._data_iterator
)
def perturb(self, data):
noise = read_one_audiosegment(
self._manifest,
data.sample_rate,
self._rng,
tarred_audio=self._tarred_audio,
audio_dataset=self._data_iterator,
)
self.perturb_with_input_noise(data, noise)
def perturb_with_input_noise(self, data, noise, data_rms=None):
snr_db = self._rng.uniform(self._min_snr_db, self._max_snr_db)
if data_rms is None:
data_rms = data.rms_db
noise_gain_db = min(data_rms - noise.rms_db - snr_db, self._max_gain_db)
# logging.debug("noise: %s %s %s", snr_db, noise_gain_db, noise_record.audio_file)
# calculate noise segment to use
start_time = self._rng.uniform(0.0, noise.duration - data.duration)
if noise.duration > (start_time + data.duration):
noise.subsegment(start_time=start_time, end_time=start_time + data.duration)
# adjust gain for snr purposes and superimpose
noise.gain_db(noise_gain_db)
if noise._samples.shape[0] < data._samples.shape[0]:
noise_idx = self._rng.randint(0, data._samples.shape[0] - noise._samples.shape[0])
data._samples[noise_idx : noise_idx + noise._samples.shape[0]] += noise._samples
else:
data._samples += noise._samples
def perturb_with_foreground_noise(
self, data, noise, data_rms=None, max_noise_dur=2, max_additions=1,
):
snr_db = self._rng.uniform(self._min_snr_db, self._max_snr_db)
if not data_rms:
data_rms = data.rms_db
noise_gain_db = min(data_rms - noise.rms_db - snr_db, self._max_gain_db)
n_additions = self._rng.randint(1, max_additions)
for i in range(n_additions):
noise_dur = self._rng.uniform(0.0, max_noise_dur)
start_time = self._rng.uniform(0.0, noise.duration)
start_sample = int(round(start_time * noise.sample_rate))
end_sample = int(round(min(noise.duration, (start_time + noise_dur)) * noise.sample_rate))
noise_samples = np.copy(noise._samples[start_sample:end_sample])
# adjust gain for snr purposes and superimpose
noise_samples *= 10.0 ** (noise_gain_db / 20.0)
if noise_samples.shape[0] > data._samples.shape[0]:
noise_samples = noise_samples[0 : data._samples.shape[0]]
noise_idx = self._rng.randint(0, data._samples.shape[0] - noise_samples.shape[0])
data._samples[noise_idx : noise_idx + noise_samples.shape[0]] += noise_samples
class WhiteNoisePerturbation(Perturbation):
"""
Perturbation that adds white noise to an audio file in the training dataset.
Args:
min_level (int): Minimum level in dB at which white noise should be added
max_level (int): Maximum level in dB at which white noise should be added
rng: Random number generator
"""
def __init__(self, min_level=-90, max_level=-46, rng=None):
self.min_level = int(min_level)
self.max_level = int(max_level)
self._rng = np.random.RandomState() if rng is None else rng
def perturb(self, data):
noise_level_db = self._rng.randint(self.min_level, self.max_level, dtype='int32')
noise_signal = self._rng.randn(data._samples.shape[0]) * (10.0 ** (noise_level_db / 20.0))
data._samples += noise_signal
class RirAndNoisePerturbation(Perturbation):
"""
RIR augmentation with additive foreground and background noise.
In this implementation audio data is augmented by first convolving the audio with a Room Impulse Response
and then adding foreground noise and background noise at various SNRs. RIR, foreground and background noises
should either be supplied with a manifest file or as tarred audio files (faster).
Different sets of noise audio files based on the original sampling rate of the noise. This is useful while
training a mixed sample rate model. For example, when training a mixed model with 8 kHz and 16 kHz audio with a
target sampling rate of 16 kHz, one would want to augment 8 kHz data with 8 kHz noise rather than 16 kHz noise.
Args:
rir_manifest_path: Manifest file for RIRs
rir_tar_filepaths: Tar files, if RIR audio files are tarred
rir_prob: Probability of applying a RIR
noise_manifest_paths: Foreground noise manifest path
min_snr_db: Min SNR for foreground noise
max_snr_db: Max SNR for background noise,
noise_tar_filepaths: Tar files, if noise files are tarred
apply_noise_rir: Whether to convolve foreground noise with a a random RIR
orig_sample_rate: Original sampling rate of foreground noise audio
max_additions: Max number of times foreground noise is added to an utterance,
max_duration: Max duration of foreground noise
bg_noise_manifest_paths: Background noise manifest path
bg_min_snr_db: Min SNR for background noise
bg_max_snr_db: Max SNR for background noise
bg_noise_tar_filepaths: Tar files, if noise files are tarred
bg_orig_sample_rate: Original sampling rate of background noise audio
"""
def __init__(
self,
rir_manifest_path=None,
rir_prob=0.5,
noise_manifest_paths=None,
min_snr_db=0,
max_snr_db=50,
rir_tar_filepaths=None,
rir_shuffle_n=100,
noise_tar_filepaths=None,
apply_noise_rir=False,
orig_sample_rate=None,
max_additions=5,
max_duration=2.0,
bg_noise_manifest_paths=None,
bg_min_snr_db=10,
bg_max_snr_db=50,
bg_noise_tar_filepaths=None,
bg_orig_sample_rate=None,
):
logging.info("Called Rir aug init")
self._rir_prob = rir_prob
self._rng = random.Random()
self._rir_perturber = ImpulsePerturbation(
manifest_path=rir_manifest_path,
audio_tar_filepaths=rir_tar_filepaths,
shuffle_n=rir_shuffle_n,
shift_impulse=True,
)
self._fg_noise_perturbers = {}
self._bg_noise_perturbers = {}
if noise_manifest_paths:
for i in range(len(noise_manifest_paths)):
if orig_sample_rate is None:
orig_sr = 16000
else:
orig_sr = orig_sample_rate[i]
self._fg_noise_perturbers[orig_sr] = NoisePerturbation(
manifest_path=noise_manifest_paths[i],
min_snr_db=min_snr_db[i],
max_snr_db=max_snr_db[i],
audio_tar_filepaths=noise_tar_filepaths[i],
orig_sr=orig_sr,
)
self._max_additions = max_additions
self._max_duration = max_duration
if bg_noise_manifest_paths:
for i in range(len(bg_noise_manifest_paths)):
if bg_orig_sample_rate is None:
orig_sr = 16000
else:
orig_sr = bg_orig_sample_rate[i]
self._bg_noise_perturbers[orig_sr] = NoisePerturbation(
manifest_path=bg_noise_manifest_paths[i],
min_snr_db=bg_min_snr_db[i],
max_snr_db=bg_max_snr_db[i],
audio_tar_filepaths=bg_noise_tar_filepaths[i],
orig_sr=orig_sr,
)
self._apply_noise_rir = apply_noise_rir
def perturb(self, data):
prob = self._rng.uniform(0.0, 1.0)
if prob < self._rir_prob:
self._rir_perturber.perturb(data)
orig_sr = data.orig_sr
if orig_sr not in self._fg_noise_perturbers:
orig_sr = max(self._fg_noise_perturbers.keys())
fg_perturber = self._fg_noise_perturbers[orig_sr]
orig_sr = data.orig_sr
if orig_sr not in self._bg_noise_perturbers:
orig_sr = max(self._bg_noise_perturbers.keys())
bg_perturber = self._bg_noise_perturbers[orig_sr]
data_rms = data.rms_db
noise = fg_perturber.get_one_noise_sample(data.sample_rate)
if self._apply_noise_rir:
self._rir_perturber.perturb(noise)
fg_perturber.perturb_with_foreground_noise(
data, noise, data_rms=data_rms, max_noise_dur=self._max_duration, max_additions=self._max_additions
)
noise = bg_perturber.get_one_noise_sample(data.sample_rate)
bg_perturber.perturb_with_input_noise(data, noise, data_rms=data_rms)
class TranscodePerturbation(Perturbation):
"""
Audio codec augmentation. This implementation uses sox to transcode audio with low rate audio codecs,
so users need to make sure that the installed sox version supports the codecs used here (G711 and amr-nb).
Args:
rng: Random number generator
"""
def __init__(self, rng=None):
self._rng = np.random.RandomState() if rng is None else rng
self._codecs = ["g711", "amr-nb"]
def perturb(self, data):
att_factor = 0.8
max_level = np.max(np.abs(data._samples))
norm_factor = att_factor / max_level
norm_samples = norm_factor * data._samples
orig_f = NamedTemporaryFile(suffix=".wav")
sf.write(orig_f.name, norm_samples.transpose(), 16000)
codec_ind = random.randint(0, len(self._codecs) - 1)
if self._codecs[codec_ind] == "amr-nb":
transcoded_f = NamedTemporaryFile(suffix="_amr.wav")
rates = list(range(0, 8))
rate = rates[random.randint(0, len(rates) - 1)]
_ = subprocess.check_output(
f"sox {orig_f.name} -V0 -C {rate} -t amr-nb - | sox -t amr-nb - -V0 -b 16 -r 16000 {transcoded_f.name}",
shell=True,
)
elif self._codecs[codec_ind] == "g711":
transcoded_f = NamedTemporaryFile(suffix="_g711.wav")
_ = subprocess.check_output(
f"sox {orig_f.name} -V0 -r 8000 -c 1 -e a-law {transcoded_f.name}", shell=True
)
new_data = AudioSegment.from_file(transcoded_f.name, target_sr=16000)
data._samples = new_data._samples[0 : data._samples.shape[0]]
return
perturbation_types = {
"speed": SpeedPerturbation,
"time_stretch": TimeStretchPerturbation,
"gain": GainPerturbation,
"impulse": ImpulsePerturbation,
"shift": ShiftPerturbation,
"noise": NoisePerturbation,
"white_noise": WhiteNoisePerturbation,
"rir_noise_aug": RirAndNoisePerturbation,
"transcode_aug": TranscodePerturbation,
}
def register_perturbation(name: str, perturbation: Perturbation):
if name in perturbation_types.keys():
raise KeyError(
f"Perturbation with the name {name} exists. " f"Type of perturbation : {perturbation_types[name]}."
)
perturbation_types[name] = perturbation
class AudioAugmentor(object):
def __init__(self, perturbations=None, rng=None):
self._rng = random.Random() if rng is None else rng
self._pipeline = perturbations if perturbations is not None else []
def perturb(self, segment):
for (prob, p) in self._pipeline:
if self._rng.random() < prob:
p.perturb(segment)
return
def max_augmentation_length(self, length):
newlen = length
for (prob, p) in self._pipeline:
newlen = p.max_augmentation_length(newlen)
return newlen
@classmethod
def from_config(cls, config):
ptbs = []
for p in config:
if p['aug_type'] not in perturbation_types:
logging.warning("%s perturbation not known. Skipping.", p['aug_type'])
continue
perturbation = perturbation_types[p['aug_type']]
ptbs.append((p['prob'], perturbation(**p['cfg'])))
return cls(perturbations=ptbs)
def process_augmentations(augmenter) -> Optional[AudioAugmentor]:
"""Process list of online data augmentations.
Accepts either an AudioAugmentor object with pre-defined augmentations,
or a dictionary that points to augmentations that have been defined.
If a dictionary is passed, must follow the below structure:
Dict[str, Dict[str, Any]]: Which refers to a dictionary of string
names for augmentations, defined in `asr/parts/perturb.py`.
The inner dictionary may contain key-value arguments of the specific
augmentation, along with an essential key `prob`. `prob` declares the
probability of the augmentation being applied, and must be a float
value in the range [0, 1].
# Example in YAML config file
Augmentations are generally applied only during training, so we can add
these augmentations to our yaml config file, and modify the behaviour
for training and evaluation.
```yaml
AudioToSpeechLabelDataLayer:
... # Parameters shared between train and evaluation time
train:
augmentor:
shift:
prob: 0.5
min_shift_ms: -5.0
max_shift_ms: 5.0
white_noise:
prob: 1.0
min_level: -90
max_level: -46
...
eval:
...
```
Then in the training script,
```python
import copy
from ruamel.yaml import YAML
yaml = YAML(typ="safe")
with open(model_config) as f:
params = yaml.load(f)
# Train Config for Data Loader
train_dl_params = copy.deepcopy(params["AudioToTextDataLayer"])
train_dl_params.update(params["AudioToTextDataLayer"]["train"])
del train_dl_params["train"]
del train_dl_params["eval"]
data_layer_train = nemo_asr.AudioToTextDataLayer(
...,
**train_dl_params,
)
# Evaluation Config for Data Loader
eval_dl_params = copy.deepcopy(params["AudioToTextDataLayer"])
eval_dl_params.update(params["AudioToTextDataLayer"]["eval"])
del eval_dl_params["train"]
del eval_dl_params["eval"]
data_layer_eval = nemo_asr.AudioToTextDataLayer(
...,
**eval_dl_params,
)
```
# Registering your own Augmentations
To register custom augmentations to obtain the above convenience of
the declaring the augmentations in YAML, you can put additional keys in
`perturbation_types` dictionary as follows.
```python
from nemo.collections.asr.parts import perturb
# Define your own perturbation here
class CustomPerturbation(perturb.Perturbation):
...
perturb.register_perturbation(name_of_perturbation, CustomPerturbation)
```
Args:
augmenter: AudioAugmentor object or
dictionary of str -> kwargs (dict) which is parsed and used
to initialize an AudioAugmentor.
Note: It is crucial that each individual augmentation has
a keyword `prob`, that defines a float probability in the
the range [0, 1] of this augmentation being applied.
If this keyword is not present, then the augmentation is
disabled and a warning is logged.
Returns: AudioAugmentor object
"""
if augmenter is None:
return None
if isinstance(augmenter, AudioAugmentor):
return augmenter
if not type(augmenter) in {dict, DictConfig}:
raise ValueError("Cannot parse augmenter. Must be a dict or an AudioAugmentor object ")
if isinstance(augmenter, DictConfig):
augmenter = OmegaConf.to_container(augmenter, resolve=True)
augmenter = copy.deepcopy(augmenter)
augmentations = []
for augment_name, augment_kwargs in augmenter.items():
prob = augment_kwargs.get('prob', None)
if prob is None:
raise KeyError(
f'Augmentation "{augment_name}" will not be applied as '
f'keyword argument "prob" was not defined for this augmentation.'
)
else:
_ = augment_kwargs.pop('prob')
if prob < 0.0 or prob > 1.0:
raise ValueError("`prob` must be a float value between 0 and 1.")
try:
augmentation = perturbation_types[augment_name](**augment_kwargs)
augmentations.append([prob, augmentation])
except KeyError:
raise KeyError(f"Invalid perturbation name. Allowed values : {perturbation_types.keys()}")
augmenter = AudioAugmentor(perturbations=augmentations)
return augmenter
class AugmentationDataset(IterableDataset):
"""
A class that loads tarred audio files and cycles over the files in the dataset.
Accepts a single comma-separated JSON manifest file (in the same style as for the AudioToCharDataset/AudioToBPEDataset),
as well as the path(s) to the tarball(s) containing the wav files. Each line of the manifest should
contain the information for one audio file, including at least the transcript and name of the audio
file within the tarball.
Valid formats for the audio_tar_filepaths argument include:
(1) a single string that can be brace-expanded, e.g. 'path/to/audio.tar' or 'path/to/audio_{1..100}.tar.gz', or
(2) a list of file paths that will not be brace-expanded, e.g. ['audio_1.tar', 'audio_2.tar', ...].
Note: For brace expansion in (1), there may be cases where `{x..y}` syntax cannot be used due to shell interference.
This occurs most commonly inside SLURM scripts. Therefore we provide a few equivalent replacements.
Supported opening braces - { <=> (, [, < and the special tag _OP_.
Supported closing braces - } <=> ), ], > and the special tag _CL_.
For SLURM based tasks, we suggest the use of the special tags for ease of use.
See the WebDataset documentation for more information about accepted data and input formats.
"""
def __init__(self, manifest_path: str, tar_filepaths: Union[str, List[str]], shuffle_n: int = 128):
self._manifest = collections.ASRAudioText(manifest_path, parser=parsers.make_parser([]), index_by_file_id=True)
if isinstance(tar_filepaths, str):
# Replace '(' and '[' with '{'
brace_keys_open = ['(', '[', '<', '_OP_']
for bkey in brace_keys_open:
if bkey in tar_filepaths:
tar_filepaths = tar_filepaths.replace(bkey, "{")
# Replace ')' and ']' with '}'
brace_keys_close = [')', ']', '>', '_CL_']
for bkey in brace_keys_close:
if bkey in tar_filepaths:
tar_filepaths = tar_filepaths.replace(bkey, "}")
self.audio_dataset = wd.WebDataset(urls=tar_filepaths, nodesplitter=None)
if shuffle_n > 0:
self.audio_dataset = self.audio_dataset.shuffle(shuffle_n)
else:
logging.info("WebDataset will not shuffle files within the tar files.")
self.audio_dataset = self.audio_dataset.rename(audio='wav', key='__key__').to_tuple('audio', 'key')
self.audio_iter = iter(self.audio_dataset)
def __len__(self):
return len(self._manifest)
def __iter__(self):
return self
def __next__(self):
while True:
try:
audio_bytes, audio_filename = next(self.audio_iter)
except StopIteration:
self.audio_iter = iter(self.audio_dataset)
audio_bytes, audio_filename = next(self.audio_iter)
file_id, _ = os.path.splitext(os.path.basename(audio_filename))
# Convert audio bytes to IO stream for processing (for SoundFile to read)
audio_file = io.BytesIO(audio_bytes)
return audio_file, file_id
@@ -0,0 +1,270 @@
# Copyright (c) 2020, 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.
#
# Copyright (c) 2018 Ryan Leary
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This file contains code artifacts adapted from https://github.com/ryanleary/patter
import os
import random
import librosa
import numpy as np
import soundfile as sf
from kaldiio.matio import read_kaldi
from kaldiio.utils import open_like_kaldi
from pydub import AudioSegment as Audio
from pydub.exceptions import CouldntDecodeError
from nemo.utils import logging
available_formats = sf.available_formats()
sf_supported_formats = ["." + i.lower() for i in available_formats.keys()]
class AudioSegment(object):
"""Monaural audio segment abstraction.
:param samples: Audio samples [num_samples x num_channels].
:type samples: ndarray.float32
:param sample_rate: Audio sample rate.
:type sample_rate: int
:raises TypeError: If the sample data type is not float or int.
"""
def __init__(self, samples, sample_rate, target_sr=None, trim=False, trim_db=60, orig_sr=None):
"""Create audio segment from samples.
Samples are convert float32 internally, with int scaled to [-1, 1].
"""
samples = self._convert_samples_to_float32(samples)
if target_sr is not None and target_sr != sample_rate:
samples = librosa.core.resample(samples, sample_rate, target_sr)
sample_rate = target_sr
if trim:
samples, _ = librosa.effects.trim(samples, trim_db)
self._samples = samples
self._sample_rate = sample_rate
if self._samples.ndim >= 2:
self._samples = np.mean(self._samples, 1)
self._orig_sr = orig_sr if orig_sr is not None else sample_rate
def __eq__(self, other):
"""Return whether two objects are equal."""
if type(other) is not type(self):
return False
if self._sample_rate != other._sample_rate:
return False
if self._samples.shape != other._samples.shape:
return False
if np.any(self.samples != other._samples):
return False
return True
def __ne__(self, other):
"""Return whether two objects are unequal."""
return not self.__eq__(other)
def __str__(self):
"""Return human-readable representation of segment."""
return "%s: num_samples=%d, sample_rate=%d, duration=%.2fsec, rms=%.2fdB" % (
type(self),
self.num_samples,
self.sample_rate,
self.duration,
self.rms_db,
)
@staticmethod
def _convert_samples_to_float32(samples):
"""Convert sample type to float32.
Audio sample type is usually integer or float-point.
Integers will be scaled to [-1, 1] in float32.
"""
float32_samples = samples.astype('float32')
if samples.dtype in np.sctypes['int']:
bits = np.iinfo(samples.dtype).bits
float32_samples *= 1.0 / 2 ** (bits - 1)
elif samples.dtype in np.sctypes['float']:
pass
else:
raise TypeError("Unsupported sample type: %s." % samples.dtype)
return float32_samples
@classmethod
def from_file(
cls, audio_file, target_sr=None, int_values=False, offset=0, duration=0, trim=False, orig_sr=None,
):
"""
Load a file supported by librosa and return as an AudioSegment.
:param audio_file: path of file to load
:param target_sr: the desired sample rate
:param int_values: if true, load samples as 32-bit integers
:param offset: offset in seconds when loading audio
:param duration: duration in seconds when loading audio
:return: numpy array of samples
"""
samples = None
if not isinstance(audio_file, str) or os.path.splitext(audio_file)[-1] in sf_supported_formats:
try:
with sf.SoundFile(audio_file, 'r') as f:
dtype = 'int32' if int_values else 'float32'
sample_rate = f.samplerate
if offset > 0:
f.seek(int(offset * sample_rate))
if duration > 0:
samples = f.read(int(duration * sample_rate), dtype=dtype)
else:
samples = f.read(dtype=dtype)
samples = samples.transpose()
except RuntimeError as e:
logging.error(
f"Loading {audio_file} via SoundFile raised RuntimeError: `{e}`. "
f"NeMo will fallback to loading via pydub."
)
elif isinstance(audio_file, str) and audio_file.strip()[-1] == "|":
f = open_like_kaldi(audio_file, "rb")
sample_rate, samples = read_kaldi(f)
if offset > 0:
samples = samples[int(offset * sample_rate) :]
if duration > 0:
samples = samples[: int(duration * sample_rate)]
if not int_values:
abs_max_value = np.abs(samples).max()
samples = np.array(samples, dtype=np.float) / abs_max_value
if samples is None:
try:
samples = Audio.from_file(audio_file)
sample_rate = samples.frame_rate
if offset > 0:
# pydub does things in milliseconds
seconds = offset * 1000
samples = samples[int(seconds * sample_rate) :]
if duration > 0:
seconds = duration * 1000
samples = samples[: int(seconds)]
samples = np.array(samples.get_array_of_samples())
except CouldntDecodeError as e:
logging.error(f"Loading {audio_file} via pydub raised CouldntDecodeError: `{e}`.")
return cls(samples, sample_rate, target_sr=target_sr, trim=trim, orig_sr=orig_sr)
@classmethod
def segment_from_file(cls, audio_file, target_sr=None, n_segments=0, trim=False, orig_sr=None):
"""Grabs n_segments number of samples from audio_file randomly from the
file as opposed to at a specified offset.
Note that audio_file can be either the file path, or a file-like object.
"""
try:
with sf.SoundFile(audio_file, 'r') as f:
sample_rate = f.samplerate
if n_segments > 0 and len(f) > n_segments:
max_audio_start = len(f) - n_segments
audio_start = random.randint(0, max_audio_start)
f.seek(audio_start)
samples = f.read(n_segments, dtype='float32')
else:
samples = f.read(dtype='float32')
samples = samples.transpose()
except RuntimeError as e:
logging.error(f"Loading {audio_file} via SoundFile raised RuntimeError: `{e}`.")
samples = samples.transpose()
return cls(samples, sample_rate, target_sr=target_sr, trim=trim, orig_sr=orig_sr)
@property
def samples(self):
return self._samples.copy()
@property
def sample_rate(self):
return self._sample_rate
@property
def num_samples(self):
return self._samples.shape[0]
@property
def duration(self):
return self._samples.shape[0] / float(self._sample_rate)
@property
def rms_db(self):
mean_square = np.mean(self._samples ** 2)
return 10 * np.log10(mean_square)
@property
def orig_sr(self):
return self._orig_sr
def gain_db(self, gain):
self._samples *= 10.0 ** (gain / 20.0)
def pad(self, pad_size, symmetric=False):
"""Add zero padding to the sample. The pad size is given in number
of samples.
If symmetric=True, `pad_size` will be added to both sides. If false,
`pad_size`
zeros will be added only to the end.
"""
self._samples = np.pad(self._samples, (pad_size if symmetric else 0, pad_size), mode='constant',)
def subsegment(self, start_time=None, end_time=None):
"""Cut the AudioSegment between given boundaries.
Note that this is an in-place transformation.
:param start_time: Beginning of subsegment in seconds.
:type start_time: float
:param end_time: End of subsegment in seconds.
:type end_time: float
:raise ValueError: If start_time or end_time is incorrectly set,
e.g. out
of bounds in time.
"""
start_time = 0.0 if start_time is None else start_time
end_time = self.duration if end_time is None else end_time
if start_time < 0.0:
start_time = self.duration + start_time
if end_time < 0.0:
end_time = self.duration + end_time
if start_time < 0.0:
raise ValueError("The slice start position (%f s) is out of bounds." % start_time)
if end_time < 0.0:
raise ValueError("The slice end position (%f s) is out of bounds." % end_time)
if start_time > end_time:
raise ValueError(
"The slice start position (%f s) is later than the end position (%f s)." % (start_time, end_time)
)
if end_time > self.duration:
raise ValueError("The slice end position (%f s) is out of bounds (> %f s)" % (end_time, self.duration))
start_sample = int(round(start_time * self._sample_rate))
end_sample = int(round(end_time * self._sample_rate))
self._samples = self._samples[start_sample:end_sample]
@@ -0,0 +1,13 @@
# Copyright (c) 2021, 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.
@@ -0,0 +1,190 @@
# Copyright (c) 2020, 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 torch
from torch import nn as nn
from torch.nn import LayerNorm
from nemo.collections.asr.parts.submodules.multi_head_attention import (
MultiHeadAttention,
RelPositionMultiHeadAttention,
)
from nemo.collections.asr.parts.utils.activations import Swish
__all__ = ['ConformerConvolution', 'ConformerFeedForward', 'ConformerLayer']
class ConformerLayer(torch.nn.Module):
"""A single block of the Conformer encoder.
Args:
d_model (int): input dimension of MultiheadAttentionMechanism and PositionwiseFeedForward
d_ff (int): hidden dimension of PositionwiseFeedForward
n_heads (int): number of heads for multi-head attention
conv_kernel_size (int): kernel size for depthwise convolution in convolution module
dropout (float): dropout probabilities for linear layers
dropout_att (float): dropout probabilities for attention distributions
"""
def __init__(
self,
d_model,
d_ff,
self_attention_model='rel_pos',
n_heads=4,
conv_kernel_size=31,
dropout=0.1,
dropout_att=0.1,
pos_bias_u=None,
pos_bias_v=None,
):
super(ConformerLayer, self).__init__()
self.self_attention_model = self_attention_model
self.n_heads = n_heads
self.fc_factor = 0.5
# first feed forward module
self.norm_feed_forward1 = LayerNorm(d_model)
self.feed_forward1 = ConformerFeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout)
# convolution module
self.norm_conv = LayerNorm(d_model)
self.conv = ConformerConvolution(d_model=d_model, kernel_size=conv_kernel_size)
# multi-headed self-attention module
self.norm_self_att = LayerNorm(d_model)
if self_attention_model == 'rel_pos':
self.self_attn = RelPositionMultiHeadAttention(
n_head=n_heads, n_feat=d_model, dropout_rate=dropout_att, pos_bias_u=pos_bias_u, pos_bias_v=pos_bias_v
)
elif self_attention_model == 'abs_pos':
self.self_attn = MultiHeadAttention(n_head=n_heads, n_feat=d_model, dropout_rate=dropout_att)
else:
raise ValueError(
f"'{self_attention_model}' is not not a valid value for 'self_attention_model', "
f"valid values can be from ['rel_pos', 'abs_pos']"
)
# second feed forward module
self.norm_feed_forward2 = LayerNorm(d_model)
self.feed_forward2 = ConformerFeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout)
self.dropout = nn.Dropout(dropout)
self.norm_out = LayerNorm(d_model)
def forward(self, x, att_mask=None, pos_emb=None, pad_mask=None):
"""
Args:
x (torch.Tensor): input signals (B, T, d_model)
att_mask (torch.Tensor): attention masks(B, T, T)
pos_emb (torch.Tensor): (L, 1, d_model)
pad_mask (torch.tensor): padding mask
Returns:
x (torch.Tensor): (B, T, d_model)
"""
residual = x
x = self.norm_feed_forward1(x)
x = self.feed_forward1(x)
x = self.fc_factor * self.dropout(x) + residual
residual = x
x = self.norm_self_att(x)
if self.self_attention_model == 'rel_pos':
x = self.self_attn(query=x, key=x, value=x, mask=att_mask, pos_emb=pos_emb)
elif self.self_attention_model == 'abs_pos':
x = self.self_attn(query=x, key=x, value=x, mask=att_mask)
else:
x = None
x = self.dropout(x) + residual
residual = x
x = self.norm_conv(x)
x = self.conv(x, pad_mask)
x = self.dropout(x) + residual
residual = x
x = self.norm_feed_forward2(x)
x = self.feed_forward2(x)
x = self.fc_factor * self.dropout(x) + residual
x = self.norm_out(x)
return x
class ConformerConvolution(nn.Module):
"""The convolution module for the Conformer model.
Args:
d_model (int): hidden dimension
kernel_size (int): kernel size for depthwise convolution
"""
def __init__(self, d_model, kernel_size):
super(ConformerConvolution, self).__init__()
assert (kernel_size - 1) % 2 == 0
self.d_model = d_model
self.pointwise_conv1 = nn.Conv1d(
in_channels=d_model, out_channels=d_model * 2, kernel_size=1, stride=1, padding=0, bias=True
)
self.depthwise_conv = nn.Conv1d(
in_channels=d_model,
out_channels=d_model,
kernel_size=kernel_size,
stride=1,
padding=(kernel_size - 1) // 2,
groups=d_model,
bias=True,
)
self.batch_norm = nn.BatchNorm1d(d_model)
self.activation = Swish()
self.pointwise_conv2 = nn.Conv1d(
in_channels=d_model, out_channels=d_model, kernel_size=1, stride=1, padding=0, bias=True
)
def forward(self, x, pad_mask=None):
x = x.transpose(1, 2)
x = self.pointwise_conv1(x)
x = nn.functional.glu(x, dim=1)
if pad_mask is not None:
x.masked_fill_(pad_mask.unsqueeze(1), 0.0)
x = self.depthwise_conv(x)
x = self.batch_norm(x)
x = self.activation(x)
x = self.pointwise_conv2(x)
x = x.transpose(1, 2)
return x
class ConformerFeedForward(nn.Module):
"""
feed-forward module of Conformer model.
"""
def __init__(self, d_model, d_ff, dropout, activation=Swish()):
super(ConformerFeedForward, self).__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.activation = activation
self.dropout = nn.Dropout(p=dropout)
self.linear2 = nn.Linear(d_ff, d_model)
def forward(self, x):
x = self.linear1(x)
x = self.activation(x)
x = self.dropout(x)
x = self.linear2(x)
return x
@@ -0,0 +1,960 @@
# Copyright (c) 2020, 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 math
from typing import Callable, List, Optional, Tuple
import torch
import torch.nn as nn
from torch import Tensor
from torch.nn.init import _calculate_correct_fan
from torch.nn.modules.utils import _single
from nemo.collections.asr.parts.utils.activations import Swish
from nemo.utils import logging
try:
from pytorch_quantization import calib
from pytorch_quantization import nn as quant_nn
from pytorch_quantization import quant_modules
from pytorch_quantization.tensor_quant import QuantDescriptor
PYTORCH_QUANTIZATION_AVAILABLE = True
except ImportError:
PYTORCH_QUANTIZATION_AVAILABLE = False
jasper_activations = {"hardtanh": nn.Hardtanh, "relu": nn.ReLU, "selu": nn.SELU, "swish": Swish, "silu": nn.SiLU}
def tds_uniform_(tensor, mode='fan_in'):
"""
Uniform Initialization from the paper [Sequence-to-Sequence Speech Recognition with Time-Depth Separable Convolutions](https://www.isca-speech.org/archive/Interspeech_2019/pdfs/2460.pdf)
Normalized to -
.. math::
\text{bound} = \text{2} \times \sqrt{\frac{1}{\text{fan\_mode}}}
Args:
tensor: an n-dimensional `torch.Tensor`
mode: either ``'fan_in'`` (default) or ``'fan_out'``. Choosing ``'fan_in'``
preserves the magnitude of the variance of the weights in the
forward pass. Choosing ``'fan_out'`` preserves the magnitudes in the
backwards pass.
"""
fan = _calculate_correct_fan(tensor, mode)
gain = 2.0 # sqrt(4.0) = 2
std = gain / math.sqrt(fan) # sqrt(4.0 / fan_in)
bound = std # Calculate uniform bounds from standard deviation
with torch.no_grad():
return tensor.uniform_(-bound, bound)
def tds_normal_(tensor, mode='fan_in'):
"""
Normal Initialization from the paper [Sequence-to-Sequence Speech Recognition with Time-Depth Separable Convolutions](https://www.isca-speech.org/archive/Interspeech_2019/pdfs/2460.pdf)
Normalized to -
.. math::
\text{bound} = \text{2} \times \sqrt{\frac{1}{\text{fan\_mode}}}
Args:
tensor: an n-dimensional `torch.Tensor`
mode: either ``'fan_in'`` (default) or ``'fan_out'``. Choosing ``'fan_in'``
preserves the magnitude of the variance of the weights in the
forward pass. Choosing ``'fan_out'`` preserves the magnitudes in the
backwards pass.
"""
fan = _calculate_correct_fan(tensor, mode)
gain = 2.0
std = gain / math.sqrt(fan) # sqrt(4.0 / fan_in)
bound = std # Calculate uniform bounds from standard deviation
with torch.no_grad():
return tensor.normal_(0.0, bound)
def init_weights(m, mode: Optional[str] = 'xavier_uniform'):
if isinstance(m, MaskedConv1d):
init_weights(m.conv, mode)
if isinstance(m, (nn.Conv1d, nn.Linear)):
if mode is not None:
if mode == 'xavier_uniform':
nn.init.xavier_uniform_(m.weight, gain=1.0)
elif mode == 'xavier_normal':
nn.init.xavier_normal_(m.weight, gain=1.0)
elif mode == 'kaiming_uniform':
nn.init.kaiming_uniform_(m.weight, nonlinearity="relu")
elif mode == 'kaiming_normal':
nn.init.kaiming_normal_(m.weight, nonlinearity="relu")
elif mode == 'tds_uniform':
tds_uniform_(m.weight)
elif mode == 'tds_normal':
tds_normal_(m.weight)
else:
raise ValueError("Unknown Initialization mode: {0}".format(mode))
elif isinstance(m, nn.BatchNorm1d):
if m.track_running_stats:
m.running_mean.zero_()
m.running_var.fill_(1)
m.num_batches_tracked.zero_()
if m.affine:
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
def compute_new_kernel_size(kernel_size, kernel_width):
new_kernel_size = max(int(kernel_size * kernel_width), 1)
# If kernel is even shape, round up to make it odd
if new_kernel_size % 2 == 0:
new_kernel_size += 1
return new_kernel_size
def get_same_padding(kernel_size, stride, dilation) -> int:
if stride > 1 and dilation > 1:
raise ValueError("Only stride OR dilation may be greater than 1")
return (dilation * (kernel_size - 1)) // 2
def get_asymtric_padding(kernel_size, stride, dilation, future_context):
if stride > 1 and dilation > 1:
raise ValueError("Only stride OR dilation may be greater than 1")
left_context = kernel_size - 1 - future_context
right_context = future_context
symmetric_padding = get_same_padding(kernel_size, stride, dilation)
if kernel_size <= future_context:
# kernel size is smaller than future context, equivalent to using entire context of kernel
# simply return symmetric padding for this scenario
logging.warning(
f"Future context window is larger than the kernel size!\n"
f"Left context = {left_context} | Right context = greater than {right_context} | "
f"Kernel size = {kernel_size}\n"
f"Switching to symmetric padding (left context = right context = {symmetric_padding})"
)
return symmetric_padding
if left_context < symmetric_padding:
logging.warning(
f"Future context window is larger than half the kernel size!\n"
f"Conv layer therefore uses more future information than past to compute its output!\n"
f"Left context = {left_context} | Right context = {right_context} | "
f"Kernel size = {kernel_size}"
)
if dilation > 1:
left_context = dilation * kernel_size - 1 - dilation * future_context
right_context = dilation * future_context
return (left_context, right_context)
return (left_context, right_context)
class StatsPoolLayer(nn.Module):
def __init__(self, feat_in, pool_mode='xvector'):
super().__init__()
self.feat_in = 0
if pool_mode == 'gram':
gram = True
super_vector = False
elif pool_mode == 'superVector':
gram = True
super_vector = True
else:
gram = False
super_vector = False
if gram:
self.feat_in += feat_in ** 2
else:
self.feat_in += 2 * feat_in
if super_vector and gram:
self.feat_in += 2 * feat_in
self.gram = gram
self.super = super_vector
def forward(self, encoder_output):
mean = encoder_output.mean(dim=-1) # Time Axis
std = encoder_output.std(dim=-1)
pooled = torch.cat([mean, std], dim=-1)
if self.gram:
time_len = encoder_output.shape[-1]
# encoder_output = encoder_output
cov = encoder_output.bmm(encoder_output.transpose(2, 1)) # cov matrix
cov = cov.view(cov.shape[0], -1) / time_len
if self.gram and not self.super:
return cov
if self.super and self.gram:
pooled = torch.cat([pooled, cov], dim=-1)
return pooled
class MaskedConv1d(nn.Module):
__constants__ = ["use_conv_mask", "real_out_channels", "heads"]
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
heads=-1,
bias=False,
use_mask=True,
quantize=False,
):
super(MaskedConv1d, self).__init__()
if not (heads == -1 or groups == in_channels):
raise ValueError("Only use heads for depthwise convolutions")
self.real_out_channels = out_channels
if heads != -1:
in_channels = heads
out_channels = heads
groups = heads
# preserve original padding
self._padding = padding
# if padding is a tuple/list, it is considered as asymmetric padding
if type(padding) in (tuple, list):
self.pad_layer = nn.ConstantPad1d(padding, value=0.0)
# reset padding for conv since pad_layer will handle this
padding = 0
else:
self.pad_layer = None
if PYTORCH_QUANTIZATION_AVAILABLE and quantize:
self.conv = quant_nn.QuantConv1d(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
)
elif not PYTORCH_QUANTIZATION_AVAILABLE and quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
self.conv = nn.Conv1d(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
)
self.use_mask = use_mask
self.heads = heads
# Calculations for "same" padding cache
self.same_padding = (self.conv.stride[0] == 1) and (
2 * self.conv.padding[0] == self.conv.dilation[0] * (self.conv.kernel_size[0] - 1)
)
if self.pad_layer is None:
self.same_padding_asymmetric = False
else:
self.same_padding_asymmetric = (self.conv.stride[0] == 1) and (
sum(self._padding) == self.conv.dilation[0] * (self.conv.kernel_size[0] - 1)
)
# `self.lens` caches consecutive integers from 0 to `self.max_len` that are used to compute the mask for a
# batch. Recomputed to bigger size as needed. Stored on a device of the latest batch lens.
if self.use_mask:
self.max_len = 0
self.lens = None
def get_seq_len(self, lens):
if self.same_padding or self.same_padding_asymmetric:
return lens
if self.pad_layer is None:
return (
lens + 2 * self.conv.padding[0] - self.conv.dilation[0] * (self.conv.kernel_size[0] - 1) - 1
) // self.conv.stride[0] + 1
else:
return (
lens + sum(self._padding) - self.conv.dilation[0] * (self.conv.kernel_size[0] - 1) - 1
) // self.conv.stride[0] + 1
def forward(self, x, lens):
if self.use_mask:
max_len = x.size(2)
if max_len > self.max_len:
self.lens = torch.arange(max_len)
self.max_len = max_len
self.lens = self.lens.to(lens.device)
mask = self.lens[:max_len].unsqueeze(0) < lens.unsqueeze(1)
x = x * mask.unsqueeze(1).to(device=x.device)
lens = self.get_seq_len(lens)
# asymmtric pad if necessary
if self.pad_layer is not None:
x = self.pad_layer(x)
sh = x.shape
if self.heads != -1:
x = x.view(-1, self.heads, sh[-1])
out = self.conv(x)
if self.heads != -1:
out = out.view(sh[0], self.real_out_channels, -1)
return out, lens
class GroupShuffle(nn.Module):
def __init__(self, groups, channels):
super(GroupShuffle, self).__init__()
self.groups = groups
self.channels_per_group = channels // groups
def forward(self, x):
sh = x.shape
x = x.view(-1, self.groups, self.channels_per_group, sh[-1])
x = torch.transpose(x, 1, 2).contiguous()
x = x.view(-1, self.groups * self.channels_per_group, sh[-1])
return x
class SqueezeExcite(nn.Module):
def __init__(
self,
channels: int,
reduction_ratio: int,
context_window: int = -1,
interpolation_mode: str = 'nearest',
activation: Optional[Callable] = None,
quantize: bool = False,
):
"""
Squeeze-and-Excitation sub-module.
Args:
channels: Input number of channels.
reduction_ratio: Reduction ratio for "squeeze" layer.
context_window: Integer number of timesteps that the context
should be computed over, using stride 1 average pooling.
If value < 1, then global context is computed.
interpolation_mode: Interpolation mode of timestep dimension.
Used only if context window is > 1.
The modes available for resizing are: `nearest`, `linear` (3D-only),
`bilinear`, `area`
activation: Intermediate activation function used. Must be a
callable activation function.
"""
super(SqueezeExcite, self).__init__()
self.interpolation_mode = interpolation_mode
self._quantize = quantize
self.pool = None # prepare a placeholder which will be updated
self.change_context_window(context_window=context_window)
if activation is None:
activation = nn.ReLU(inplace=True)
if PYTORCH_QUANTIZATION_AVAILABLE and quantize:
self.fc = nn.Sequential(
quant_nn.QuantLinear(channels, channels // reduction_ratio, bias=False),
activation,
quant_nn.QuantLinear(channels // reduction_ratio, channels, bias=False),
)
elif not PYTORCH_QUANTIZATION_AVAILABLE and quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
self.fc = nn.Sequential(
nn.Linear(channels, channels // reduction_ratio, bias=False),
activation,
nn.Linear(channels // reduction_ratio, channels, bias=False),
)
self.gap = nn.AdaptiveAvgPool1d(1)
def forward(self, x):
# The use of negative indices on the transpose allow for expanded SqueezeExcite
batch, channels, timesteps = x.size()[:3]
# Computes in float32 to avoid instabilities during training with AMP.
with torch.cuda.amp.autocast(enabled=False):
x = x.float()
if timesteps < self.context_window:
y = self.gap(x)
else:
y = self.pool(x) # [B, C, T - context_window + 1]
y = y.transpose(1, -1) # [B, T - context_window + 1, C]
y = self.fc(y) # [B, T - context_window + 1, C]
y = y.transpose(1, -1) # [B, C, T - context_window + 1]
if self.context_window > 0:
y = torch.nn.functional.interpolate(y, size=timesteps, mode=self.interpolation_mode)
y = torch.sigmoid(y)
return x * y
def change_context_window(self, context_window: int):
"""
Update the context window of the SqueezeExcitation module, in-place if possible.
Will update the pooling layer to either nn.AdaptiveAvgPool1d() (for global SE) or nn.AvgPool1d()
(for limited context SE).
If only the context window is changing but still a limited SE context block - then
the earlier instance of nn.AvgPool1d() will be updated.
Args:
context_window: An integer representing the number of input timeframes that will be used
to compute the context. Each timeframe corresponds to a single window stride of the
STFT features.
Say the window_stride = 0.01s, then a context window of 128 represents 128 * 0.01 s
of context to compute the Squeeze step.
"""
if hasattr(self, 'context_window'):
logging.info(f"Changing Squeeze-Excitation context window from {self.context_window} to {context_window}")
self.context_window = int(context_window)
if self.context_window <= 0:
if PYTORCH_QUANTIZATION_AVAILABLE and self._quantize:
if not isinstance(self.pool, quant_nn.QuantAdaptiveAvgPool1d(1)):
self.pool = quant_nn.QuantAdaptiveAvgPool1d(1) # context window = T
elif not PYTORCH_QUANTIZATION_AVAILABLE and self._quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
if not isinstance(self.pool, nn.AdaptiveAvgPool1d):
self.pool = nn.AdaptiveAvgPool1d(1) # context window = T
else:
if PYTORCH_QUANTIZATION_AVAILABLE and self._quantize:
if not isinstance(self.pool, quant_nn.QuantAvgPool1d):
self.pool = quant_nn.QuantAvgPool1d(self.context_window, stride=1)
elif not PYTORCH_QUANTIZATION_AVAILABLE and self._quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
if not isinstance(self.pool, nn.AvgPool1d):
self.pool = nn.AvgPool1d(self.context_window, stride=1)
else:
# update the context window
self.pool.kernel_size = _single(self.context_window)
class JasperBlock(nn.Module):
"""
Constructs a single "Jasper" block. With modified parameters, also constructs other blocks for models
such as `QuartzNet` and `Citrinet`.
- For `Jasper` : `separable` flag should be False
- For `QuartzNet` : `separable` flag should be True
- For `Citrinet` : `separable` flag and `se` flag should be True
Note that above are general distinctions, each model has intricate differences that expand over
multiple such blocks.
For further information about the differences between models which use JasperBlock, please review
the configs for ASR models found in the ASR examples directory.
Args:
inplanes: Number of input channels.
planes: Number of output channels.
repeat: Number of repeated sub-blocks (R) for this block.
kernel_size: Convolution kernel size across all repeated sub-blocks.
kernel_size_factor: Floating point scale value that is multiplied with kernel size,
then rounded down to nearest odd integer to compose the kernel size. Defaults to 1.0.
stride: Stride of the convolutional layers.
dilation: Integer which defined dilation factor of kernel. Note that when dilation > 1, stride must
be equal to 1.
padding: String representing type of padding. Currently only supports "same" padding,
which symmetrically pads the input tensor with zeros.
dropout: Floating point value, determins percentage of output that is zeroed out.
activation: String representing activation functions. Valid activation functions are :
{"hardtanh": nn.Hardtanh, "relu": nn.ReLU, "selu": nn.SELU, "swish": Swish}.
Defaults to "relu".
residual: Bool that determined whether a residual branch should be added or not.
All residual branches are constructed using a pointwise convolution kernel, that may or may not
perform strided convolution depending on the parameter `residual_mode`.
groups: Number of groups for Grouped Convolutions. Defaults to 1.
separable: Bool flag that describes whether Time-Channel depthwise separable convolution should be
constructed, or ordinary convolution should be constructed.
heads: Number of "heads" for the masked convolution. Defaults to -1, which disables it.
normalization: String that represents type of normalization performed. Can be one of
"batch", "group", "instance" or "layer" to compute BatchNorm1D, GroupNorm1D, InstanceNorm or
LayerNorm (which are special cases of GroupNorm1D).
norm_groups: Number of groups used for GroupNorm (if `normalization` == "group").
residual_mode: String argument which describes whether the residual branch should be simply
added ("add") or should first stride, then add ("stride_add"). Required when performing stride on
parallel branch as well as utilizing residual add.
residual_panes: Number of residual panes, used for Jasper-DR models. Please refer to the paper.
conv_mask: Bool flag which determines whether to utilize masked convolutions or not. In general,
it should be set to True.
se: Bool flag that determines whether Squeeze-and-Excitation layer should be used.
se_reduction_ratio: Integer value, which determines to what extend the hidden dimension of the SE
intermediate step should be reduced. Larger values reduce number of parameters, but also limit
the effectiveness of SE layers.
se_context_window: Integer value determining the number of timesteps that should be utilized in order
to compute the averaged context window. Defaults to -1, which means it uses global context - such
that all timesteps are averaged. If any positive integer is used, it will utilize limited context
window of that size.
se_interpolation_mode: String used for interpolation mode of timestep dimension for SE blocks.
Used only if context window is > 1.
The modes available for resizing are: `nearest`, `linear` (3D-only),
`bilinear`, `area`.
stride_last: Bool flag that determines whether all repeated blocks should stride at once,
(stride of S^R when this flag is False) or just the last repeated block should stride
(stride of S when this flag is True).
future_context: Int value that determins how many "right" / "future" context frames will be utilized
when calculating the output of the conv kernel. All calculations are done for odd kernel sizes only.
By default, this is -1, which is recomputed as the symmetric padding case.
When future_context >= 0, will compute the asymmetric padding as follows :
(left context, right context) = [K - 1 - future_context, future_context]
Determining an exact formula to limit future context is dependent on global layout of the model.
As such, we provide both "local" and "global" guidelines below.
Local context limit (should always be enforced)
- future context should be <= half the kernel size for any given layer
- future context > kernel size defaults to symmetric kernel
- future context of layer = number of future frames * width of each frame (dependent on stride)
Global context limit (should be carefully considered)
- future context should be layed out in an ever reducing pattern. Initial layers should restrict
future context less than later layers, since shallow depth (and reduced stride) means each frame uses
less amounts of future context.
- Beyond a certain point, future context should remain static for a given stride level. This is
the upper bound of the amount of future context that can be provided to the model on a global scale.
- future context is calculated (roughly) as - (2 ^ stride) * (K // 2) number of future frames.
This resultant value should be bound to some global maximum number of future seconds of audio (in ms).
Note: In the special case where K < future_context, it is assumed that the kernel is too small to limit
its future context, so symmetric padding is used instead.
Note: There is no explicit limitation on the amount of future context used, as long as
K > future_context constraint is maintained. This might lead to cases where future_context is
more than half the actual kernel size K! In such cases, the conv layer is utilizing more of the future
context than its current and past context to compute the output. While this is possible to do,
it is not recommended and the layer will raise a warning to notify the user of such cases.
It is advised to simply use symmetric padding for such cases.
Example:
Say we have a model that performs 8x stride and receives spectrogram frames with stride of 0.01s.
Say we wish to upper bound future context to 80 ms.
Layer ID, Kernel Size, Stride, Future Context, Global Context
0, K=5, S=1, FC=8, GC= 2 * (2^0) = 2 * 0.01 ms (special case, K < FC so use symmetric pad)
1, K=7, S=1, FC=3, GC= 3 * (2^0) = 3 * 0.01 ms (note that symmetric pad here uses 3 FC frames!)
2, K=11, S=2, FC=4, GC= 4 * (2^1) = 8 * 0.01 ms (note that symmetric pad here uses 5 FC frames!)
3, K=15, S=1, FC=4, GC= 4 * (2^1) = 8 * 0.01 ms (note that symmetric pad here uses 7 FC frames!)
4, K=21, S=2, FC=2, GC= 2 * (2^2) = 8 * 0.01 ms (note that symmetric pad here uses 10 FC frames!)
5, K=25, S=2, FC=1, GC= 1 * (2^3) = 8 * 0.01 ms (note that symmetric pad here uses 14 FC frames!)
6, K=29, S=1, FC=1, GC= 1 * (2^3) = 8 * 0.01 ms ...
quantize: Bool flag whether to quantize the Convolutional blocks.
"""
__constants__ = ["conv_mask", "separable", "residual_mode", "res", "mconv"]
def __init__(
self,
inplanes,
planes,
repeat=3,
kernel_size=11,
kernel_size_factor=1,
stride=1,
dilation=1,
padding='same',
dropout=0.2,
activation=None,
residual=True,
groups=1,
separable=False,
heads=-1,
normalization="batch",
norm_groups=1,
residual_mode='add',
residual_panes=[],
conv_mask=False,
se=False,
se_reduction_ratio=16,
se_context_window=-1,
se_interpolation_mode='nearest',
stride_last=False,
future_context: int = -1,
quantize=False,
):
super(JasperBlock, self).__init__()
if padding != "same":
raise ValueError("currently only 'same' padding is supported")
kernel_size_factor = float(kernel_size_factor)
if type(kernel_size) in (list, tuple):
kernel_size = [compute_new_kernel_size(k, kernel_size_factor) for k in kernel_size]
else:
kernel_size = compute_new_kernel_size(kernel_size, kernel_size_factor)
if future_context < 0:
padding_val = get_same_padding(kernel_size[0], stride[0], dilation[0])
else:
padding_val = get_asymtric_padding(kernel_size[0], stride[0], dilation[0], future_context)
self.conv_mask = conv_mask
self.separable = separable
self.residual_mode = residual_mode
self.se = se
self.quantize = quantize
inplanes_loop = inplanes
conv = nn.ModuleList()
for _ in range(repeat - 1):
# Stride last means only the last convolution in block will have stride
if stride_last:
stride_val = [1]
else:
stride_val = stride
conv.extend(
self._get_conv_bn_layer(
inplanes_loop,
planes,
kernel_size=kernel_size,
stride=stride_val,
dilation=dilation,
padding=padding_val,
groups=groups,
heads=heads,
separable=separable,
normalization=normalization,
norm_groups=norm_groups,
quantize=quantize,
)
)
conv.extend(self._get_act_dropout_layer(drop_prob=dropout, activation=activation))
inplanes_loop = planes
conv.extend(
self._get_conv_bn_layer(
inplanes_loop,
planes,
kernel_size=kernel_size,
stride=stride,
dilation=dilation,
padding=padding_val,
groups=groups,
heads=heads,
separable=separable,
normalization=normalization,
norm_groups=norm_groups,
quantize=quantize,
)
)
if se:
conv.append(
SqueezeExcite(
planes,
reduction_ratio=se_reduction_ratio,
context_window=se_context_window,
interpolation_mode=se_interpolation_mode,
activation=activation,
quantize=quantize,
)
)
self.mconv = conv
res_panes = residual_panes.copy()
self.dense_residual = residual
if residual:
res_list = nn.ModuleList()
if residual_mode == 'stride_add':
stride_val = stride
else:
stride_val = [1]
if len(residual_panes) == 0:
res_panes = [inplanes]
self.dense_residual = False
for ip in res_panes:
res = nn.ModuleList(
self._get_conv_bn_layer(
ip,
planes,
kernel_size=1,
normalization=normalization,
norm_groups=norm_groups,
stride=stride_val,
quantize=quantize,
)
)
res_list.append(res)
self.res = res_list
if PYTORCH_QUANTIZATION_AVAILABLE and self.quantize:
self.residual_quantizer = quant_nn.TensorQuantizer(quant_nn.QuantConv2d.default_quant_desc_input)
elif not PYTORCH_QUANTIZATION_AVAILABLE and quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
self.res = None
self.mout = nn.Sequential(*self._get_act_dropout_layer(drop_prob=dropout, activation=activation))
def _get_conv(
self,
in_channels,
out_channels,
kernel_size=11,
stride=1,
dilation=1,
padding=0,
bias=False,
groups=1,
heads=-1,
separable=False,
quantize=False,
):
use_mask = self.conv_mask
if use_mask:
return MaskedConv1d(
in_channels,
out_channels,
kernel_size,
stride=stride,
dilation=dilation,
padding=padding,
bias=bias,
groups=groups,
heads=heads,
use_mask=use_mask,
quantize=quantize,
)
else:
if PYTORCH_QUANTIZATION_AVAILABLE and quantize:
return quant_nn.QuantConv1d(
in_channels,
out_channels,
kernel_size,
stride=stride,
dilation=dilation,
padding=padding,
bias=bias,
groups=groups,
)
elif not PYTORCH_QUANTIZATION_AVAILABLE and quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
return nn.Conv1d(
in_channels,
out_channels,
kernel_size,
stride=stride,
dilation=dilation,
padding=padding,
bias=bias,
groups=groups,
)
def _get_conv_bn_layer(
self,
in_channels,
out_channels,
kernel_size=11,
stride=1,
dilation=1,
padding=0,
bias=False,
groups=1,
heads=-1,
separable=False,
normalization="batch",
norm_groups=1,
quantize=False,
):
if norm_groups == -1:
norm_groups = out_channels
if separable:
layers = [
self._get_conv(
in_channels,
in_channels,
kernel_size,
stride=stride,
dilation=dilation,
padding=padding,
bias=bias,
groups=in_channels,
heads=heads,
quantize=quantize,
),
self._get_conv(
in_channels,
out_channels,
kernel_size=1,
stride=1,
dilation=1,
padding=0,
bias=bias,
groups=groups,
quantize=quantize,
),
]
else:
layers = [
self._get_conv(
in_channels,
out_channels,
kernel_size,
stride=stride,
dilation=dilation,
padding=padding,
bias=bias,
groups=groups,
quantize=quantize,
)
]
if normalization == "group":
layers.append(nn.GroupNorm(num_groups=norm_groups, num_channels=out_channels))
elif normalization == "instance":
layers.append(nn.GroupNorm(num_groups=out_channels, num_channels=out_channels))
elif normalization == "layer":
layers.append(nn.GroupNorm(num_groups=1, num_channels=out_channels))
elif normalization == "batch":
layers.append(nn.BatchNorm1d(out_channels, eps=1e-3, momentum=0.1))
else:
raise ValueError(
f"Normalization method ({normalization}) does not match" f" one of [batch, layer, group, instance]."
)
if groups > 1:
layers.append(GroupShuffle(groups, out_channels))
return layers
def _get_act_dropout_layer(self, drop_prob=0.2, activation=None):
if activation is None:
activation = nn.Hardtanh(min_val=0.0, max_val=20.0)
layers = [activation, nn.Dropout(p=drop_prob)]
return layers
def forward(self, input_: Tuple[List[Tensor], Optional[Tensor]]):
"""
Forward pass of the module.
Args:
input_: The input is a tuple of two values - the preprocessed audio signal as well as the lengths
of the audio signal. The audio signal is padded to the shape [B, D, T] and the lengths are
a torch vector of length B.
Returns:
The output of the block after processing the input through `repeat` number of sub-blocks,
as well as the lengths of the encoded audio after padding/striding.
"""
# type: (Tuple[List[Tensor], Optional[Tensor]]) -> Tuple[List[Tensor], Optional[Tensor]] # nopep8
lens_orig = None
xs = input_[0]
if len(input_) == 2:
xs, lens_orig = input_
# compute forward convolutions
out = xs[-1]
lens = lens_orig
for i, l in enumerate(self.mconv):
# if we're doing masked convolutions, we need to pass in and
# possibly update the sequence lengths
# if (i % 4) == 0 and self.conv_mask:
if isinstance(l, MaskedConv1d):
out, lens = l(out, lens)
else:
out = l(out)
# compute the residuals
if self.res is not None:
for i, layer in enumerate(self.res):
res_out = xs[i]
for j, res_layer in enumerate(layer):
if isinstance(res_layer, MaskedConv1d):
res_out, _ = res_layer(res_out, lens_orig)
else:
res_out = res_layer(res_out)
if self.residual_mode == 'add' or self.residual_mode == 'stride_add':
if PYTORCH_QUANTIZATION_AVAILABLE and self.quantize:
out = self.residual_quantizer(out) + res_out
elif not PYTORCH_QUANTIZATION_AVAILABLE and self.quantize:
raise ImportError(
"pytorch-quantization is not installed. Install from "
"https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization."
)
else:
out = out + res_out
else:
out = torch.max(out, res_out)
# compute the output
out = self.mout(out)
if self.res is not None and self.dense_residual:
return xs + [out], lens
return [out], lens
@@ -0,0 +1,340 @@
# Copyright (c) 2020, 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.
#
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
#
# 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.
#
"""
Part of this code is adopted from https://github.com/espnet/espnet
"""
import math
import numpy as np
import torch
import torch.nn as nn
__all__ = [
'RelPositionMultiHeadAttention',
'RelPositionalEncoding',
'PositionalEncoding',
]
class MultiHeadAttention(nn.Module):
"""Multi-Head Attention layer of Transformer.
Args:
n_head (int): number of heads
n_feat (int): size of the features
dropout_rate (float): dropout rate
"""
def __init__(self, n_head, n_feat, dropout_rate):
"""Construct an MultiHeadedAttention object."""
super(MultiHeadAttention, self).__init__()
assert n_feat % n_head == 0
# We assume d_v always equals d_k
self.d_k = n_feat // n_head
self.h = n_head
self.linear_q = nn.Linear(n_feat, n_feat)
self.linear_k = nn.Linear(n_feat, n_feat)
self.linear_v = nn.Linear(n_feat, n_feat)
self.linear_out = nn.Linear(n_feat, n_feat)
self.attn = None
self.dropout = nn.Dropout(p=dropout_rate)
def forward_qkv(self, query, key, value):
"""Transforms query, key and value.
Args:
query (torch.Tensor): (batch, time1, size)
key (torch.Tensor): (batch, time2, size)
value (torch.Tensor): (batch, time2, size)
returns:
q (torch.Tensor): (batch, head, time1, size)
k (torch.Tensor): (batch, head, time2, size)
v (torch.Tensor): (batch, head, time2, size)
"""
n_batch = query.size(0)
q = self.linear_q(query).view(n_batch, -1, self.h, self.d_k)
k = self.linear_k(key).view(n_batch, -1, self.h, self.d_k)
v = self.linear_v(value).view(n_batch, -1, self.h, self.d_k)
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
return q, k, v
def forward_attention(self, value, scores, mask):
"""Compute attention context vector.
Args:
value (torch.Tensor): (batch, time2, size)
scores(torch.Tensor): (batch, time1, time2)
mask(torch.Tensor): (batch, time1, time2)
returns:
value (torch.Tensor): transformed `value` (batch, time2, d_model) weighted by the attention scores
"""
n_batch = value.size(0)
if mask is not None:
mask = mask.unsqueeze(1) # (batch, 1, time1, time2)
if scores.dtype == torch.float16:
dtype = np.float16
else:
dtype = np.float32
min_value = np.finfo(dtype).min
scores = scores.masked_fill(mask, min_value)
self.attn = torch.softmax(scores, dim=-1).masked_fill(mask, 0.0) # (batch, head, time1, time2)
else:
self.attn = torch.softmax(scores, dim=-1) # (batch, head, time1, time2)
p_attn = self.dropout(self.attn)
x = torch.matmul(p_attn, value) # (batch, head, time1, d_k)
x = x.transpose(1, 2).contiguous().view(n_batch, -1, self.h * self.d_k) # (batch, time1, d_model)
return self.linear_out(x) # (batch, time1, d_model)
def forward(self, query, key, value, mask, pos_emb=None):
"""Compute 'Scaled Dot Product Attention'.
Args:
query (torch.Tensor): (batch, time1, size)
key (torch.Tensor): (batch, time2, size)
value(torch.Tensor): (batch, time2, size)
mask (torch.Tensor): (batch, time1, time2)
returns:
output (torch.Tensor): transformed `value` (batch, time1, d_model) weighted by the query dot key attention
"""
q, k, v = self.forward_qkv(query, key, value)
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
return self.forward_attention(v, scores, mask)
class RelPositionMultiHeadAttention(MultiHeadAttention):
"""Multi-Head Attention layer of Transformer-XL with support of relative positional encoding.
Paper: https://arxiv.org/abs/1901.02860
Args:
n_head (int): number of heads
n_feat (int): size of the features
dropout_rate (float): dropout rate
"""
def __init__(self, n_head, n_feat, dropout_rate, pos_bias_u, pos_bias_v):
"""Construct an RelPositionMultiHeadedAttention object."""
super().__init__(n_head, n_feat, dropout_rate)
# linear transformation for positional encoding
self.linear_pos = nn.Linear(n_feat, n_feat, bias=False)
# these two learnable biases are used in matrix c and matrix d
# as described in https://arxiv.org/abs/1901.02860 Section 3.3
if pos_bias_u is None or pos_bias_v is None:
self.pos_bias_u = nn.Parameter(torch.FloatTensor(self.h, self.d_k))
self.pos_bias_v = nn.Parameter(torch.FloatTensor(self.h, self.d_k))
# nn.init.normal_(self.pos_bias_u, 0.0, 0.02)
# nn.init.normal_(self.pos_bias_v, 0.0, 0.02)
nn.init.zeros_(self.pos_bias_u)
nn.init.zeros_(self.pos_bias_v)
else:
self.pos_bias_u = pos_bias_u
self.pos_bias_v = pos_bias_v
def rel_shift(self, x):
"""Compute relative positional encoding.
Args:
x (torch.Tensor): (batch, nheads, time, 2*time-1)
"""
x_size = x.size() # (b, h, t1, t2)
qlen = x_size[-2]
pos_len = x_size[-1]
# need to add a column of zeros on the left side of last two dimensions to perform the relative shifting
x = torch.nn.functional.pad(x, pad=(1, 0, 0, 0)) # (b, h, t1, t2+1)
x = x.view(*x.size()[:-2], pos_len + 1, qlen) # (b, h, t2+1, t1)
# need to drop the first row
x = x[:, :, 1:].view(*x_size) # (b, h, t1, t2)
return x
def forward(self, query, key, value, mask, pos_emb):
"""Compute 'Scaled Dot Product Attention' with rel. positional encoding.
Args:
query (torch.Tensor): (batch, time1, size)
key (torch.Tensor): (batch, time2, size)
value(torch.Tensor): (batch, time2, size)
mask (torch.Tensor): (batch, time1, time2)
pos_emb (torch.Tensor) : (batch, time1, size)
Returns:
output (torch.Tensor): transformed `value` (batch, time1, d_model) weighted by the query dot key attention
"""
q, k, v = self.forward_qkv(query, key, value)
q = q.transpose(1, 2) # (batch, time1, head, d_k)
n_batch_pos = pos_emb.size(0)
p = self.linear_pos(pos_emb).view(n_batch_pos, -1, self.h, self.d_k)
p = p.transpose(1, 2) # (batch, head, time1, d_k)
# (batch, head, time1, d_k)
q_with_bias_u = (q + self.pos_bias_u).transpose(1, 2)
# (batch, head, time1, d_k)
q_with_bias_v = (q + self.pos_bias_v).transpose(1, 2)
# compute attention score
# first compute matrix a and matrix c
# as described in https://arxiv.org/abs/1901.02860 Section 3.3
# (batch, head, time1, time2)
matrix_ac = torch.matmul(q_with_bias_u, k.transpose(-2, -1))
# compute matrix b and matrix d
# (batch, head, time1, time2)
matrix_bd = torch.matmul(q_with_bias_v, p.transpose(-2, -1))
matrix_bd = self.rel_shift(matrix_bd)
# drops extra elements in the matrix_bd if there are any left from rel_shift() to match the matrix_ac's size
matrix_bd = matrix_bd[:, :, :, : matrix_ac.size(-1)]
scores = (matrix_ac + matrix_bd) / math.sqrt(self.d_k) # (batch, head, time1, time2)
return self.forward_attention(v, scores, mask)
class PositionalEncoding(torch.nn.Module):
"""Fixed sinusoidal positional encoding.
Args:
d_model (int): embedding dim
dropout_rate (float): dropout rate
max_len (int): maximum input length
xscale (bool): whether to scale the input by sqrt(d_model)
dropout_rate_emb (float): dropout rate for the positional embeddings
"""
def __init__(self, d_model, dropout_rate, max_len=5000, xscale=None, dropout_rate_emb=0.0):
"""Construct an PositionalEncoding object."""
super(PositionalEncoding, self).__init__()
self.d_model = d_model
self.xscale = xscale
self.dropout = torch.nn.Dropout(p=dropout_rate)
self.extend_pe(length=max_len)
if dropout_rate_emb > 0:
self.dropout_emb = nn.Dropout(dropout_rate_emb)
else:
self.dropout_emb = None
def create_pe(self, positions):
pos_length = positions.size(0)
pe = torch.zeros(pos_length, self.d_model)
div_term = torch.exp(
torch.arange(0, self.d_model, 2, dtype=torch.float32) * -(math.log(10000.0) / self.d_model)
)
pe[:, 0::2] = torch.sin(positions * div_term)
pe[:, 1::2] = torch.cos(positions * div_term)
pe = pe.unsqueeze(0)
return pe
def extend_pe(self, length):
"""Reset and extend the positional encodings if needed."""
if hasattr(self, 'pe') and self.pe.size(1) >= length:
return
positions = torch.arange(0, length, dtype=torch.float32).unsqueeze(1)
pe = self.create_pe(positions=positions)
if not hasattr(self, 'pe'):
self.register_buffer('pe', pe, persistent=False)
self.pe = pe
def forward(self, x: torch.Tensor):
"""Adds positional encoding.
Args:
x (torch.Tensor): Input. Its shape is (batch, time, feature_size)
Returns:
x+pos_emb (torch.Tensor): Its shape is (batch, time, feature_size)
pos_emb (torch.Tensor): Its shape is (1, time, feature_size)
"""
self.extend_pe(x)
if self.pe.dtype != x.dtype or self.pe.device != x.device:
self.pe = self.pe.to(device=x.device, dtype=x.dtype)
if self.xscale:
x = x * self.xscale
pos_emb = self.pe[:, : x.size(1)]
if self.dropout_emb:
pos_emb = self.dropout_emb(pos_emb)
x = x + pos_emb
return self.dropout(x), pos_emb
class RelPositionalEncoding(PositionalEncoding):
"""Relative positional encoding for TransformerXL's layers
See : Appendix B in https://arxiv.org/abs/1901.02860
Args:
d_model (int): embedding dim
dropout_rate (float): dropout rate
max_len (int): maximum input length
xscale (bool): whether to scale the input by sqrt(d_model)
dropout_rate_emb (float): dropout rate for the positional embeddings
"""
def __init__(self, d_model, dropout_rate, max_len=5000, xscale=None, dropout_rate_emb=0.0):
super().__init__(d_model, dropout_rate, max_len, xscale=xscale)
if dropout_rate_emb > 0:
self.dropout_emb = nn.Dropout(dropout_rate_emb)
else:
self.dropout_emb = None
self.max_len = max_len
def extend_pe(self, length):
"""Reset and extend the positional encodings if needed."""
needed_size = 2 * (length - 1) + 1
if hasattr(self, 'pe') and self.pe.size(1) >= needed_size:
return
# positions would be from negative numbers to positive
# positive positions would be used for left positions and negative for right positions
positions = torch.arange(length - 1, -length, -1, dtype=torch.float32).unsqueeze(1)
pe = self.create_pe(positions=positions)
if not hasattr(self, 'pe'):
self.register_buffer('pe', pe, persistent=False)
self.pe = pe
def forward(self, x):
"""Compute positional encoding.
Args:
x (torch.Tensor): Input. Its shape is (batch, time, feature_size)
Returns:
x (torch.Tensor): Its shape is (batch, time, feature_size)
pos_emb (torch.Tensor): Its shape is (1, time, feature_size)
"""
self.extend_pe(length=x.size(1))
if self.pe.dtype != x.dtype or self.pe.device != x.device:
self.pe = self.pe.to(device=x.device, dtype=x.dtype)
if self.xscale:
x = x * self.xscale
# center_pos would be the index of position 0
# negative positions would be used for right and positive for left tokens
# for input of length L, 2*L-1 positions are needed, positions from (L-1) to -(L-1)
center_pos = self.pe.size(1) // 2
start_pos = center_pos - x.size(1) + 1
end_pos = center_pos + x.size(1)
pos_emb = self.pe[:, start_pos:end_pos]
if self.dropout_emb:
pos_emb = self.dropout_emb(pos_emb)
return self.dropout(x), pos_emb
@@ -0,0 +1,791 @@
# Copyright (c) 2020, 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.
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
#
# 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 copy
from dataclasses import dataclass
from typing import List, Optional, Union
import numpy as np
import torch
from tqdm import tqdm
from nemo.collections.asr.modules import rnnt_abstract
from nemo.collections.asr.parts.utils.rnnt_utils import Hypothesis, NBestHypotheses
from nemo.core.classes import Typing, typecheck
from nemo.core.neural_types import AcousticEncodedRepresentation, HypothesisType, LengthsType, NeuralType
class BeamRNNTInfer(Typing):
"""
Beam Search implementation ported from ESPNet implementation -
https://github.com/espnet/espnet/blob/master/espnet/nets/beam_search_transducer.py
Sequence level beam decoding or batched-beam decoding, performed auto-repressively
depending on the search type chosen.
Args:
decoder_model: rnnt_utils.AbstractRNNTDecoder implementation.
joint_model: rnnt_utils.AbstractRNNTJoint implementation.
beam_size: number of beams for beam search. Must be a positive integer >= 1.
If beam size is 1, defaults to stateful greedy search.
This greedy search might result in slightly different results than
the greedy results obtained by GreedyRNNTInfer due to implementation differences.
For accurate greedy results, please use GreedyRNNTInfer or GreedyBatchedRNNTInfer.
search_type: str representing the type of beam search to perform.
Must be one of ['beam', 'tsd', 'alsd']. 'nsc' is currently not supported.
Algoritm used:
`beam` - basic beam search strategy. Larger beams generally result in better decoding,
however the time required for the search also grows steadily.
`tsd` - time synchronous decoding. Please refer to the paper:
[Alignment-Length Synchronous Decoding for RNN Transducer](https://ieeexplore.ieee.org/document/9053040)
for details on the algorithm implemented.
Time synchronous decoding (TSD) execution time grows by the factor T * max_symmetric_expansions.
For longer sequences, T is greater, and can therefore take a long time for beams to obtain
good results. This also requires greater memory to execute.
`alsd` - alignment-length synchronous decoding. Please refer to the paper:
[Alignment-Length Synchronous Decoding for RNN Transducer](https://ieeexplore.ieee.org/document/9053040)
for details on the algorithm implemented.
Alignment-length synchronous decoding (ALSD) execution time is faster than TSD, with growth
factor of T + U_max, where U_max is the maximum target length expected during execution.
Generally, T + U_max < T * max_symmetric_expansions. However, ALSD beams are non-unique,
therefore it is required to use larger beam sizes to achieve the same (or close to the same)
decoding accuracy as TSD.
For a given decoding accuracy, it is possible to attain faster decoding via ALSD than TSD.
score_norm: bool, whether to normalize the scores of the log probabilities.
return_best_hypothesis: bool, decides whether to return a single hypothesis (the best out of N),
or return all N hypothesis (sorted with best score first). The container class changes based
this flag -
When set to True (default), returns a single Hypothesis.
When set to False, returns a NBestHypotheses container, which contains a list of Hypothesis.
# The following arguments are specific to the chosen `search_type`
tsd_max_sym_exp_per_step: Used for `search_type=tsd`. The maximum symmetric expansions allowed
per timestep during beam search. Larger values should be used to attempt decoding of longer
sequences, but this in turn increases execution time and memory usage.
alsd_max_target_len: Used for `search_type=alsd`. The maximum expected target sequence length
during beam search. Larger values allow decoding of longer sequences at the expense of
execution time and memory.
# The following two flags are placeholders and unused until `nsc` implementation is stabilized.
nsc_max_timesteps_expansion: Unused int.
nsc_prefix_alpha: Unused int.
preserve_alignments: Bool flag which preserves the history of alignments generated during
beam decoding (sample). When set to true, the Hypothesis will contain
the non-null value for `alignments` in it. Here, `alignments` is a List of List of ints.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
NOTE: `preserve_alignments` is an invalid argument for any `search_type`
other than basic beam search.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"encoder_output": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"predictions": [NeuralType(elements_type=HypothesisType())]}
def __init__(
self,
decoder_model: rnnt_abstract.AbstractRNNTDecoder,
joint_model: rnnt_abstract.AbstractRNNTJoint,
beam_size: int,
search_type: str = 'default',
score_norm: bool = True,
return_best_hypothesis: bool = True,
tsd_max_sym_exp_per_step: Optional[int] = 50,
alsd_max_target_len: Union[int, float] = 1.0,
nsc_max_timesteps_expansion: int = 1,
nsc_prefix_alpha: int = 1,
preserve_alignments: bool = False,
):
self.decoder = decoder_model
self.joint = joint_model
self.blank = decoder_model.blank_idx
self.vocab_size = decoder_model.vocab_size
self.search_type = search_type
self.return_best_hypothesis = return_best_hypothesis
if beam_size < 1:
raise ValueError("Beam search size cannot be less than 1!")
self.beam_size = beam_size
self.score_norm = score_norm
if self.beam_size == 1:
self.search_algorithm = self.greedy_search
elif search_type == "default":
self.search_algorithm = self.default_beam_search
elif search_type == "tsd":
self.search_algorithm = self.time_sync_decoding
elif search_type == "alsd":
self.search_algorithm = self.align_length_sync_decoding
elif search_type == "nsc":
raise NotImplementedError("`nsc` (Constrained Beam Search) has not been implemented.")
# self.search_algorithm = self.nsc_beam_search
else:
raise NotImplementedError(
f"The search type ({search_type}) supplied is not supported!\n"
f"Please use one of : (default, tsd, alsd, nsc)"
)
if tsd_max_sym_exp_per_step is None:
tsd_max_sym_exp_per_step = -1
if search_type in ['tsd', 'alsd', 'nsc'] and not self.decoder.blank_as_pad:
raise ValueError(
f"Search type was chosen as '{search_type}', however the decoder module provided "
f"does not support the `blank` token as a pad value. {search_type} requires "
f"the blank token as pad value support in order to perform batched beam search."
f"Please chose one of the other beam search methods, or re-train your model "
f"with this support."
)
self.tsd_max_symmetric_expansion_per_step = tsd_max_sym_exp_per_step
self.alsd_max_target_length = alsd_max_target_len
self.nsc_max_timesteps_expansion = nsc_max_timesteps_expansion
self.nsc_prefix_alpha = nsc_prefix_alpha
self.preserve_alignments = preserve_alignments
@typecheck()
def __call__(
self, encoder_output: torch.Tensor, encoded_lengths: torch.Tensor
) -> Union[Hypothesis, NBestHypotheses]:
"""Perform general beam search.
Args:
encoder_output: Encoded speech features (B, T_max, D_enc)
encoded_lengths: Lengths of the encoder outputs
Returns:
Either a list containing a single Hypothesis (when `return_best_hypothesis=True`,
otherwise a list containing a single NBestHypotheses, which itself contains a list of
Hypothesis. This list is sorted such that the best hypothesis is the first element.
"""
# Preserve decoder and joint training state
decoder_training_state = self.decoder.training
joint_training_state = self.joint.training
with torch.no_grad():
# Apply optional preprocessing
encoder_output = encoder_output.transpose(1, 2) # (B, T, D)
self.decoder.eval()
self.joint.eval()
hypotheses = []
with tqdm(
range(encoder_output.size(0)),
desc='Beam search progress:',
total=encoder_output.size(0),
unit='sample',
) as idx_gen:
# Freeze the decoder and joint to prevent recording of gradients
# during the beam loop.
with self.decoder.as_frozen(), self.joint.as_frozen():
# Decode every sample in the batch independently.
for batch_idx in idx_gen:
inseq = encoder_output[batch_idx : batch_idx + 1, :, :] # [1, T, D]
logitlen = encoded_lengths[batch_idx]
# Execute the specific search strategy
nbest_hyps = self.search_algorithm(inseq, logitlen) # sorted list of hypothesis
# Pack the result
if self.return_best_hypothesis:
best_hypothesis = nbest_hyps[0] # type: Hypothesis
else:
best_hypothesis = NBestHypotheses(nbest_hyps) # type: NBestHypotheses
hypotheses.append(best_hypothesis)
self.decoder.train(decoder_training_state)
self.joint.train(joint_training_state)
return (hypotheses,)
def sort_nbest(self, hyps: List[Hypothesis]) -> List[Hypothesis]:
"""Sort hypotheses by score or score given sequence length.
Args:
hyps: list of hypotheses
Return:
hyps: sorted list of hypotheses
"""
if self.score_norm:
return sorted(hyps, key=lambda x: x.score / len(x.y_sequence), reverse=True)
else:
return sorted(hyps, key=lambda x: x.score, reverse=True)
def greedy_search(self, h: torch.Tensor, encoded_lengths: torch.Tensor) -> List[Hypothesis]:
"""Greedy search implementation for transducer.
Generic case when beam size = 1. Results might differ slightly due to implementation details
as compared to `GreedyRNNTInfer` and `GreedyBatchRNNTInfer`.
Args:
h: Encoded speech features (1, T_max, D_enc)
Returns:
hyp: 1-best decoding results
"""
if self.preserve_alignments:
# Alignments is a 2-dimensional dangling list representing T x U
alignments = [[]]
else:
alignments = None
# Initialize zero state vectors
dec_state = self.decoder.initialize_state(h)
# Construct initial hypothesis
hyp = Hypothesis(
score=0.0, y_sequence=[self.blank], dec_state=dec_state, timestep=[-1], length=encoded_lengths
)
cache = {}
# Initialize state and first token
y, state, _ = self.decoder.score_hypothesis(hyp, cache)
for i in range(int(encoded_lengths)):
hi = h[:, i : i + 1, :] # [1, 1, D]
not_blank = True
symbols_added = 0
while not_blank:
ytu = torch.log_softmax(self.joint.joint(hi, y), dim=-1) # [1, 1, 1, V + 1]
ytu = ytu[0, 0, 0, :] # [V + 1]
# max() requires float
if ytu.dtype != torch.float32:
ytu = ytu.float()
logp, pred = torch.max(ytu, dim=-1) # [1, 1]
pred = pred.item()
if self.preserve_alignments:
# insert logits into last timestep
alignments[-1].append(pred)
if pred == self.blank:
not_blank = False
if self.preserve_alignments:
# convert Ti-th logits into a torch array
alignments.append([]) # blank buffer for next timestep
else:
# Update state and current sequence
hyp.y_sequence.append(int(pred))
hyp.score += float(logp)
hyp.dec_state = state
hyp.timestep.append(i)
# Compute next state and token
y, state, _ = self.decoder.score_hypothesis(hyp, cache)
symbols_added += 1
# Remove trailing empty list of alignments
if self.preserve_alignments:
if len(alignments[-1]) == 0:
del alignments[-1]
# attach alignments to hypothesis
hyp.alignments = alignments
return [hyp]
def default_beam_search(self, h: torch.Tensor, encoded_lengths: torch.Tensor) -> List[Hypothesis]:
"""Beam search implementation.
Args:
x: Encoded speech features (1, T_max, D_enc)
Returns:
nbest_hyps: N-best decoding results
"""
# Initialize states
beam = min(self.beam_size, self.vocab_size)
beam_k = min(beam, (self.vocab_size - 1))
blank_tensor = torch.tensor([self.blank], device=h.device, dtype=torch.long)
# Precompute some constants for blank position
ids = list(range(self.vocab_size + 1))
ids.remove(self.blank)
# Used when blank token is first vs last token
if self.blank == 0:
index_incr = 1
else:
index_incr = 0
# Initialize zero vector states
dec_state = self.decoder.initialize_state(h)
# Initialize first hypothesis for the beam (blank)
kept_hyps = [Hypothesis(score=0.0, y_sequence=[self.blank], dec_state=dec_state, timestep=[-1], length=0)]
cache = {}
if self.preserve_alignments:
kept_hyps[0].alignments = [[]]
for i in range(int(encoded_lengths)):
hi = h[:, i : i + 1, :] # [1, 1, D]
hyps = kept_hyps
kept_hyps = []
while True:
max_hyp = max(hyps, key=lambda x: x.score)
hyps.remove(max_hyp)
# update decoder state and get next score
y, state, lm_tokens = self.decoder.score_hypothesis(max_hyp, cache) # [1, 1, D]
# get next token
ytu = torch.log_softmax(self.joint.joint(hi, y), dim=-1) # [1, 1, 1, V + 1]
ytu = ytu[0, 0, 0, :] # [V + 1]
# remove blank token before top k
top_k = ytu[ids].topk(beam_k, dim=-1)
# Two possible steps - blank token or non-blank token predicted
ytu = (
torch.cat((top_k[0], ytu[self.blank].unsqueeze(0))),
torch.cat((top_k[1] + index_incr, blank_tensor)),
)
# for each possible step
for logp, k in zip(*ytu):
# construct hypothesis for step
new_hyp = Hypothesis(
score=(max_hyp.score + float(logp)),
y_sequence=max_hyp.y_sequence[:],
dec_state=max_hyp.dec_state,
lm_state=max_hyp.lm_state,
timestep=max_hyp.timestep[:],
length=encoded_lengths,
)
if self.preserve_alignments:
new_hyp.alignments = copy.deepcopy(max_hyp.alignments)
# if current token is blank, dont update sequence, just store the current hypothesis
if k == self.blank:
kept_hyps.append(new_hyp)
else:
# if non-blank token was predicted, update state and sequence and then search more hypothesis
new_hyp.dec_state = state
new_hyp.y_sequence.append(int(k))
new_hyp.timestep.append(i)
hyps.append(new_hyp)
if self.preserve_alignments:
if k == self.blank:
new_hyp.alignments[-1].append(self.blank)
else:
new_hyp.alignments[-1].append(new_hyp.y_sequence[-1])
# keep those hypothesis that have scores greater than next search generation
hyps_max = float(max(hyps, key=lambda x: x.score).score)
kept_most_prob = sorted([hyp for hyp in kept_hyps if hyp.score > hyps_max], key=lambda x: x.score,)
# If enough hypothesis have scores greater than next search generation,
# stop beam search.
if len(kept_most_prob) >= beam:
if self.preserve_alignments:
# convert Ti-th logits into a torch array
for kept_h in kept_most_prob:
kept_h.alignments.append([]) # blank buffer for next timestep
kept_hyps = kept_most_prob
break
# Remove trailing empty list of alignments
if self.preserve_alignments:
for h in kept_hyps:
if len(h.alignments[-1]) == 0:
del h.alignments[-1]
return self.sort_nbest(kept_hyps)
def time_sync_decoding(self, h: torch.Tensor, encoded_lengths: torch.Tensor) -> List[Hypothesis]:
"""Time synchronous beam search implementation.
Based on https://ieeexplore.ieee.org/document/9053040
Args:
h: Encoded speech features (1, T_max, D_enc)
Returns:
nbest_hyps: N-best decoding results
"""
if self.preserve_alignments:
raise NotImplementedError("`preseve_alignments` is not implemented for Time-Synchronous Decoding.")
# Precompute some constants for blank position
ids = list(range(self.vocab_size + 1))
ids.remove(self.blank)
# Used when blank token is first vs last token
if self.blank == 0:
index_incr = 1
else:
index_incr = 0
# prepare the batched beam states
beam = min(self.beam_size, self.vocab_size)
beam_state = self.decoder.initialize_state(
torch.zeros(beam, device=h.device, dtype=h.dtype)
) # [L, B, H], [L, B, H] (for LSTMs)
# Initialize first hypothesis for the beam (blank)
B = [
Hypothesis(
y_sequence=[self.blank],
score=0.0,
dec_state=self.decoder.batch_select_state(beam_state, 0),
timestep=[-1],
length=0,
)
]
cache = {}
for i in range(int(encoded_lengths)):
hi = h[:, i : i + 1, :]
# Update caches
A = []
C = B
h_enc = hi
# For a limited number of symmetric expansions per timestep "i"
for v in range(self.tsd_max_symmetric_expansion_per_step):
D = []
# Decode a batch of beam states and scores
beam_y, beam_state, beam_lm_tokens = self.decoder.batch_score_hypothesis(C, cache, beam_state)
# Extract the log probabilities and the predicted tokens
beam_logp = torch.log_softmax(self.joint.joint(h_enc, beam_y), dim=-1) # [B, 1, 1, V + 1]
beam_logp = beam_logp[:, 0, 0, :] # [B, V + 1]
beam_topk = beam_logp[:, ids].topk(beam, dim=-1)
seq_A = [h.y_sequence for h in A]
for j, hyp in enumerate(C):
# create a new hypothesis in A
if hyp.y_sequence not in seq_A:
# If the sequence is not in seq_A, add it as the blank token
# In this step, we dont add a token but simply update score
A.append(
Hypothesis(
score=(hyp.score + float(beam_logp[j, self.blank])),
y_sequence=hyp.y_sequence[:],
dec_state=hyp.dec_state,
lm_state=hyp.lm_state,
timestep=hyp.timestep[:],
length=encoded_lengths,
)
)
else:
# merge the existing blank hypothesis score with current score.
dict_pos = seq_A.index(hyp.y_sequence)
A[dict_pos].score = np.logaddexp(
A[dict_pos].score, (hyp.score + float(beam_logp[j, self.blank]))
)
if v < self.tsd_max_symmetric_expansion_per_step:
for j, hyp in enumerate(C):
# for each current hypothesis j
# extract the top token score and top token id for the jth hypothesis
for logp, k in zip(beam_topk[0][j], beam_topk[1][j] + index_incr):
# create new hypothesis and store in D
# Note: This loop does *not* include the blank token!
new_hyp = Hypothesis(
score=(hyp.score + float(logp)),
y_sequence=(hyp.y_sequence + [int(k)]),
dec_state=self.decoder.batch_select_state(beam_state, j),
lm_state=hyp.lm_state,
timestep=hyp.timestep[:] + [i],
length=encoded_lengths,
)
D.append(new_hyp)
# Prune beam
C = sorted(D, key=lambda x: x.score, reverse=True)[:beam]
# Prune beam
B = sorted(A, key=lambda x: x.score, reverse=True)[:beam]
return self.sort_nbest(B)
def align_length_sync_decoding(self, h: torch.Tensor, encoded_lengths: torch.Tensor) -> List[Hypothesis]:
"""Alignment-length synchronous beam search implementation.
Based on https://ieeexplore.ieee.org/document/9053040
Args:
h: Encoded speech features (1, T_max, D_enc)
Returns:
nbest_hyps: N-best decoding results
"""
if self.preserve_alignments:
raise NotImplementedError(
"`preseve_alignments` is not implemented for Alignment-length Synchronous Decoding."
)
# Precompute some constants for blank position
ids = list(range(self.vocab_size + 1))
ids.remove(self.blank)
# Used when blank token is first vs last token
if self.blank == 0:
index_incr = 1
else:
index_incr = 0
# prepare the batched beam states
beam = min(self.beam_size, self.vocab_size)
h = h[0] # [T, D]
h_length = int(encoded_lengths)
beam_state = self.decoder.initialize_state(
torch.zeros(beam, device=h.device, dtype=h.dtype)
) # [L, B, H], [L, B, H] for LSTMS
# compute u_max as either a specific static limit,
# or a multiple of current `h_length` dynamically.
if type(self.alsd_max_target_length) == float:
u_max = int(self.alsd_max_target_length * h_length)
else:
u_max = int(self.alsd_max_target_length)
# Initialize first hypothesis for the beam (blank)
B = [
Hypothesis(
y_sequence=[self.blank],
score=0.0,
dec_state=self.decoder.batch_select_state(beam_state, 0),
timestep=[-1],
length=0,
)
]
final = []
cache = {}
# ALSD runs for T + U_max steps
for i in range(h_length + u_max):
# Update caches
A = []
B_ = []
h_states = []
# preserve the list of batch indices which are added into the list
# and those which are removed from the list
# This is necessary to perform state updates in the correct batch indices later
batch_ids = list(range(len(B))) # initialize as a list of all batch ids
batch_removal_ids = [] # update with sample ids which are removed
for bid, hyp in enumerate(B):
u = len(hyp.y_sequence) - 1
t = i - u + 1
if t > (h_length - 1):
batch_removal_ids.append(bid)
continue
B_.append(hyp)
h_states.append((t, h[t]))
if B_:
# Compute the subset of batch ids which were *not* removed from the list above
sub_batch_ids = None
if len(B_) != beam:
sub_batch_ids = batch_ids
for id in batch_removal_ids:
# sub_batch_ids contains list of ids *that were not removed*
sub_batch_ids.remove(id)
# extract the states of the sub batch only.
beam_state_ = [beam_state[state_id][:, sub_batch_ids, :] for state_id in range(len(beam_state))]
else:
# If entire batch was used (none were removed), simply take all the states
beam_state_ = beam_state
# Decode a batch/sub-batch of beam states and scores
beam_y, beam_state_, beam_lm_tokens = self.decoder.batch_score_hypothesis(B_, cache, beam_state_)
# If only a subset of batch ids were updated (some were removed)
if sub_batch_ids is not None:
# For each state in the RNN (2 for LSTM)
for state_id in range(len(beam_state)):
# Update the current batch states with the sub-batch states (in the correct indices)
# These indices are specified by sub_batch_ids, the ids of samples which were updated.
beam_state[state_id][:, sub_batch_ids, :] = beam_state_[state_id][...]
else:
# If entire batch was updated, simply update all the states
beam_state = beam_state_
# h_states = list of [t, h[t]]
# so h[1] here is a h[t] of shape [D]
# Simply stack all of the h[t] within the sub_batch/batch (T <= beam)
h_enc = torch.stack([h[1] for h in h_states]) # [T=beam, D]
h_enc = h_enc.unsqueeze(1) # [B=beam, T=1, D]; batch over the beams
# Extract the log probabilities and the predicted tokens
beam_logp = torch.log_softmax(self.joint.joint(h_enc, beam_y), dim=-1) # [B=beam, 1, 1, V + 1]
beam_logp = beam_logp[:, 0, 0, :] # [B=beam, V + 1]
beam_topk = beam_logp[:, ids].topk(beam, dim=-1)
for j, hyp in enumerate(B_):
# For all updated samples in the batch, add it as the blank token
# In this step, we dont add a token but simply update score
new_hyp = Hypothesis(
score=(hyp.score + float(beam_logp[j, self.blank])),
y_sequence=hyp.y_sequence[:],
dec_state=hyp.dec_state,
lm_state=hyp.lm_state,
timestep=hyp.timestep[:],
length=i,
)
# Add blank prediction to A
A.append(new_hyp)
# If the prediction "timestep" t has reached the length of the input sequence
# we can add it to the "finished" hypothesis list.
if h_states[j][0] == (h_length - 1):
final.append(new_hyp)
# Here, we carefully select the indices of the states that we want to preserve
# for the next token (non-blank) update.
if sub_batch_ids is not None:
h_states_idx = sub_batch_ids[j]
else:
h_states_idx = j
# for each current hypothesis j
# extract the top token score and top token id for the jth hypothesis
for logp, k in zip(beam_topk[0][j], beam_topk[1][j] + index_incr):
# create new hypothesis and store in A
# Note: This loop does *not* include the blank token!
new_hyp = Hypothesis(
score=(hyp.score + float(logp)),
y_sequence=(hyp.y_sequence[:] + [int(k)]),
dec_state=self.decoder.batch_select_state(beam_state, h_states_idx),
lm_state=hyp.lm_state,
timestep=hyp.timestep[:] + [i],
length=i,
)
A.append(new_hyp)
# Prune and recombine same hypothesis
# This may cause next beam to be smaller than max beam size
# Therefore larger beam sizes may be required for better decoding.
B = sorted(A, key=lambda x: x.score, reverse=True)[:beam]
B = self.recombine_hypotheses(B)
# If B_ is empty list, then we may be able to early exit
elif len(batch_ids) == len(batch_removal_ids):
break
if final:
return self.sort_nbest(final)
else:
return B
def recombine_hypotheses(self, hypotheses: List[Hypothesis]) -> List[Hypothesis]:
"""Recombine hypotheses with equivalent output sequence.
Args:
hypotheses (list): list of hypotheses
Returns:
final (list): list of recombined hypotheses
"""
final = []
for hyp in hypotheses:
seq_final = [f.y_sequence for f in final if f.y_sequence]
if hyp.y_sequence in seq_final:
seq_pos = seq_final.index(hyp.y_sequence)
final[seq_pos].score = np.logaddexp(final[seq_pos].score, hyp.score)
else:
final.append(hyp)
return hypotheses
@dataclass
class BeamRNNTInferConfig:
beam_size: int
search_type: str = 'default'
score_norm: bool = True
return_best_hypothesis: bool = True
tsd_max_sym_exp_per_step: Optional[int] = 50
alsd_max_target_len: float = 1.0
nsc_max_timesteps_expansion: int = 1
nsc_prefix_alpha: int = 1
preserve_alignments: bool = False
@@ -0,0 +1,731 @@
# Copyright (c) 2020, 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.
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
#
# 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.
from dataclasses import dataclass
from typing import List, Optional, Union
import torch
from nemo.collections.asr.modules import rnnt_abstract
from nemo.collections.asr.parts.utils import rnnt_utils
from nemo.collections.common.parts.rnn import label_collate
from nemo.core.classes import Typing, typecheck
from nemo.core.neural_types import AcousticEncodedRepresentation, HypothesisType, LengthsType, NeuralType
def pack_hypotheses(
hypotheses: List[List[int]],
timesteps: List[List[int]],
logitlen: torch.Tensor,
alignments: Optional[List[List[int]]] = None,
) -> List[rnnt_utils.Hypothesis]:
logitlen_cpu = logitlen.to("cpu")
return [
rnnt_utils.Hypothesis(
y_sequence=torch.tensor(sent, dtype=torch.long),
score=-1.0,
timestep=timestep,
length=length,
alignments=alignments[idx] if alignments is not None else None,
)
for idx, (sent, timestep, length) in enumerate(zip(hypotheses, timesteps, logitlen_cpu))
]
class _GreedyRNNTInfer(Typing):
"""A greedy transducer decoder.
Provides a common abstraction for sample level and batch level greedy decoding.
Args:
decoder_model: rnnt_utils.AbstractRNNTDecoder implementation.
joint_model: rnnt_utils.AbstractRNNTJoint implementation.
blank_index: int index of the blank token. Can be 0 or len(vocabulary).
max_symbols_per_step: Optional int. The maximum number of symbols that can be added
to a sequence in a single time step; if set to None then there is
no limit.
preserve_alignments: Bool flag which preserves the history of alignments generated during
greedy decoding (sample / batched). When set to true, the Hypothesis will contain
the non-null value for `alignments` in it. Here, `alignments` is a List of List of ints.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"encoder_output": NeuralType(('B', 'D', 'T'), AcousticEncodedRepresentation()),
"encoded_lengths": NeuralType(tuple('B'), LengthsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"predictions": [NeuralType(elements_type=HypothesisType())]}
def __init__(
self,
decoder_model: rnnt_abstract.AbstractRNNTDecoder,
joint_model: rnnt_abstract.AbstractRNNTJoint,
blank_index: int,
max_symbols_per_step: Optional[int] = None,
preserve_alignments: bool = False,
):
super().__init__()
self.decoder = decoder_model
self.joint = joint_model
self._blank_index = blank_index
self._SOS = blank_index # Start of single index
self.max_symbols = max_symbols_per_step
self.preserve_alignments = preserve_alignments
def __call__(self, *args, **kwargs):
return self.forward(*args, **kwargs)
@torch.no_grad()
def _pred_step(
self,
label: Union[torch.Tensor, int],
hidden: Optional[torch.Tensor],
add_sos: bool = False,
batch_size: Optional[int] = None,
) -> (torch.Tensor, torch.Tensor):
"""
Common prediction step based on the AbstractRNNTDecoder implementation.
Args:
label: (int/torch.Tensor): Label or "Start-of-Signal" token.
hidden: (Optional torch.Tensor): RNN State vector
add_sos (bool): Whether to add a zero vector at the begging as "start of sentence" token.
batch_size: Batch size of the output tensor.
Returns:
g: (B, U, H) if add_sos is false, else (B, U + 1, H)
hid: (h, c) where h is the final sequence hidden state and c is
the final cell state:
h (tensor), shape (L, B, H)
c (tensor), shape (L, B, H)
"""
if isinstance(label, torch.Tensor):
# label: [batch, 1]
if label.dtype != torch.long:
label = label.long()
else:
# Label is an integer
if label == self._SOS:
return self.decoder.predict(None, hidden, add_sos=add_sos, batch_size=batch_size)
label = label_collate([[label]])
# output: [B, 1, K]
return self.decoder.predict(label, hidden, add_sos=add_sos, batch_size=batch_size)
def _joint_step(self, enc, pred, log_normalize: Optional[bool] = None):
"""
Common joint step based on AbstractRNNTJoint implementation.
Args:
enc: Output of the Encoder model. A torch.Tensor of shape [B, 1, H1]
pred: Output of the Decoder model. A torch.Tensor of shape [B, 1, H2]
log_normalize: Whether to log normalize or not. None will log normalize only for CPU.
Returns:
logits of shape (B, T=1, U=1, V + 1)
"""
with torch.no_grad():
logits = self.joint.joint(enc, pred)
if log_normalize is None:
if not logits.is_cuda: # Use log softmax only if on CPU
logits = logits.log_softmax(dim=len(logits.shape) - 1)
else:
if log_normalize:
logits = logits.log_softmax(dim=len(logits.shape) - 1)
return logits
class GreedyRNNTInfer(_GreedyRNNTInfer):
"""A greedy transducer decoder.
Sequence level greedy decoding, performed auto-repressively.
Args:
decoder_model: rnnt_utils.AbstractRNNTDecoder implementation.
joint_model: rnnt_utils.AbstractRNNTJoint implementation.
blank_index: int index of the blank token. Can be 0 or len(vocabulary).
max_symbols_per_step: Optional int. The maximum number of symbols that can be added
to a sequence in a single time step; if set to None then there is
no limit.
preserve_alignments: Bool flag which preserves the history of alignments generated during
greedy decoding (sample / batched). When set to true, the Hypothesis will contain
the non-null value for `alignments` in it. Here, `alignments` is a List of List of ints.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
"""
def __init__(
self,
decoder_model: rnnt_abstract.AbstractRNNTDecoder,
joint_model: rnnt_abstract.AbstractRNNTJoint,
blank_index: int,
max_symbols_per_step: Optional[int] = None,
preserve_alignments: bool = False,
):
super().__init__(
decoder_model=decoder_model,
joint_model=joint_model,
blank_index=blank_index,
max_symbols_per_step=max_symbols_per_step,
preserve_alignments=preserve_alignments,
)
@typecheck()
def forward(self, encoder_output: torch.Tensor, encoded_lengths: torch.Tensor):
"""Returns a list of hypotheses given an input batch of the encoder hidden embedding.
Output token is generated auto-repressively.
Args:
encoder_output: A tensor of size (batch, features, timesteps).
encoded_lengths: list of int representing the length of each sequence
output sequence.
Returns:
packed list containing batch number of sentences (Hypotheses).
"""
# Preserve decoder and joint training state
decoder_training_state = self.decoder.training
joint_training_state = self.joint.training
with torch.no_grad():
# Apply optional preprocessing
encoder_output = encoder_output.transpose(1, 2) # (B, T, D)
self.decoder.eval()
self.joint.eval()
hypotheses = []
timesteps = []
alignments = [] if self.preserve_alignments else None
# Process each sequence independently
with self.decoder.as_frozen(), self.joint.as_frozen():
for batch_idx in range(encoder_output.size(0)):
inseq = encoder_output[batch_idx, :, :].unsqueeze(1) # [T, 1, D]
logitlen = encoded_lengths[batch_idx]
sentence, timestep, alignment = self._greedy_decode(inseq, logitlen)
hypotheses.append(sentence)
timesteps.append(timestep)
if self.preserve_alignments:
alignments.append(alignment)
# Pack results into Hypotheses
packed_result = pack_hypotheses(hypotheses, timesteps, encoded_lengths, alignments=alignments)
self.decoder.train(decoder_training_state)
self.joint.train(joint_training_state)
return (packed_result,)
@torch.no_grad()
def _greedy_decode(self, x: torch.Tensor, out_len: torch.Tensor):
# x: [T, 1, D]
# out_len: [seq_len]
# Initialize blank state and empty label set
hidden = None
label = []
timesteps = []
if self.preserve_alignments:
# Alignments is a 2-dimensional dangling list representing T x U
alignments = [[]]
else:
alignments = None
# For timestep t in X_t
for time_idx in range(out_len):
# Extract encoder embedding at timestep t
# f = x[time_idx, :, :].unsqueeze(0) # [1, 1, D]
f = x.narrow(dim=0, start=time_idx, length=1)
# Setup exit flags and counter
not_blank = True
symbols_added = 0
# While blank is not predicted, or we dont run out of max symbols per timestep
while not_blank and (self.max_symbols is None or symbols_added < self.max_symbols):
# In the first timestep, we initialize the network with RNNT Blank
# In later timesteps, we provide previous predicted label as input.
last_label = self._SOS if label == [] else label[-1]
# Perform prediction network and joint network steps.
g, hidden_prime = self._pred_step(last_label, hidden)
logp = self._joint_step(f, g, log_normalize=None)[0, 0, 0, :]
del g
# torch.max(0) op doesnt exist for FP 16.
if logp.dtype != torch.float32:
logp = logp.float()
# get index k, of max prob
v, k = logp.max(0)
k = k.item() # K is the label at timestep t_s in inner loop, s >= 0.
if self.preserve_alignments:
# insert logits into last timestep
alignments[-1].append(k)
del logp
# If blank token is predicted, exit inner loop, move onto next timestep t
if k == self._blank_index:
not_blank = False
if self.preserve_alignments:
# convert Ti-th logits into a torch array
alignments.append([]) # blank buffer for next timestep
else:
# Append token to label set, update RNN state.
label.append(k)
timesteps.append(time_idx)
hidden = hidden_prime
# Increment token counter.
symbols_added += 1
# Remove trailing empty list of Alignments
if self.preserve_alignments:
if len(alignments[-1]) == 0:
del alignments[-1]
return label, timesteps, alignments
class GreedyBatchedRNNTInfer(_GreedyRNNTInfer):
"""A batch level greedy transducer decoder.
Batch level greedy decoding, performed auto-repressively.
Args:
decoder_model: rnnt_utils.AbstractRNNTDecoder implementation.
joint_model: rnnt_utils.AbstractRNNTJoint implementation.
blank_index: int index of the blank token. Can be 0 or len(vocabulary).
max_symbols_per_step: Optional int. The maximum number of symbols that can be added
to a sequence in a single time step; if set to None then there is
no limit.
preserve_alignments: Bool flag which preserves the history of alignments generated during
greedy decoding (sample / batched). When set to true, the Hypothesis will contain
the non-null value for `alignments` in it. Here, `alignments` is a List of List of ints.
The length of the list corresponds to the Acoustic Length (T).
Each value in the list (Ti) is a torch.Tensor (U), representing 1 or more targets from a vocabulary.
U is the number of target tokens for the current timestep Ti.
"""
def __init__(
self,
decoder_model: rnnt_abstract.AbstractRNNTDecoder,
joint_model: rnnt_abstract.AbstractRNNTJoint,
blank_index: int,
max_symbols_per_step: Optional[int] = None,
preserve_alignments: bool = False,
):
super().__init__(
decoder_model=decoder_model,
joint_model=joint_model,
blank_index=blank_index,
max_symbols_per_step=max_symbols_per_step,
preserve_alignments=preserve_alignments,
)
# Depending on availability of `blank_as_pad` support
# switch between more efficient batch decoding technique
if self.decoder.blank_as_pad:
self._greedy_decode = self._greedy_decode_blank_as_pad
else:
self._greedy_decode = self._greedy_decode_masked
@typecheck()
def forward(self, encoder_output: torch.Tensor, encoded_lengths: torch.Tensor):
"""Returns a list of hypotheses given an input batch of the encoder hidden embedding.
Output token is generated auto-repressively.
Args:
encoder_output: A tensor of size (batch, features, timesteps).
encoded_lengths: list of int representing the length of each sequence
output sequence.
Returns:
packed list containing batch number of sentences (Hypotheses).
"""
# Preserve decoder and joint training state
decoder_training_state = self.decoder.training
joint_training_state = self.joint.training
with torch.no_grad():
# Apply optional preprocessing
encoder_output = encoder_output.transpose(1, 2) # (B, T, D)
logitlen = encoded_lengths
self.decoder.eval()
self.joint.eval()
with self.decoder.as_frozen(), self.joint.as_frozen():
inseq = encoder_output # [B, T, D]
hypotheses, timesteps, alignments = self._greedy_decode(inseq, logitlen, device=inseq.device)
# Pack the hypotheses results
packed_result = pack_hypotheses(hypotheses, timesteps, logitlen, alignments=alignments)
del hypotheses, timesteps
self.decoder.train(decoder_training_state)
self.joint.train(joint_training_state)
return (packed_result,)
def _greedy_decode_blank_as_pad(self, x: torch.Tensor, out_len: torch.Tensor, device: torch.device):
with torch.no_grad():
# x: [B, T, D]
# out_len: [B]
# device: torch.device
# Initialize state
hidden = None
batchsize = x.shape[0]
# Output string buffer
label = [[] for _ in range(batchsize)]
timesteps = [[] for _ in range(batchsize)]
# If alignments need to be preserved, register a danling list to hold the values
if self.preserve_alignments:
# alignments is a 3-dimensional dangling list representing B x T x U
alignments = []
for _ in range(batchsize):
alignments.append([[]])
else:
alignments = None
# Last Label buffer + Last Label without blank buffer
# batch level equivalent of the last_label
last_label = torch.full([batchsize, 1], fill_value=self._blank_index, dtype=torch.long, device=device)
# Mask buffers
blank_mask = torch.full([batchsize], fill_value=0, dtype=torch.bool, device=device)
# Get max sequence length
max_out_len = out_len.max()
for time_idx in range(max_out_len):
f = x.narrow(dim=1, start=time_idx, length=1) # [B, 1, D]
# Prepare t timestamp batch variables
not_blank = True
symbols_added = 0
# Reset blank mask
blank_mask.mul_(False)
# Update blank mask with time mask
# Batch: [B, T, D], but Bi may have seq len < max(seq_lens_in_batch)
# Forcibly mask with "blank" tokens, for all sample where current time step T > seq_len
blank_mask = time_idx >= out_len
# Start inner loop
while not_blank and (self.max_symbols is None or symbols_added < self.max_symbols):
# Batch prediction and joint network steps
# If very first prediction step, submit SOS tag (blank) to pred_step.
# This feeds a zero tensor as input to AbstractRNNTDecoder to prime the state
if time_idx == 0 and symbols_added == 0:
g, hidden_prime = self._pred_step(self._SOS, hidden, batch_size=batchsize)
else:
# Perform batch step prediction of decoder, getting new states and scores ("g")
g, hidden_prime = self._pred_step(last_label, hidden, batch_size=batchsize)
# Batched joint step - Output = [B, V + 1]
logp = self._joint_step(f, g, log_normalize=None)[:, 0, 0, :]
if logp.dtype != torch.float32:
logp = logp.float()
# Get index k, of max prob for batch
v, k = logp.max(1)
del v, g
# Update blank mask with current predicted blanks
# This is accumulating blanks over all time steps T and all target steps min(max_symbols, U)
k_is_blank = k == self._blank_index
blank_mask.bitwise_or_(k_is_blank)
del k_is_blank
# If preserving alignments, check if sequence length of sample has been reached
# before adding alignment
if self.preserve_alignments:
# Insert ids into last timestep per sample
logp_vals = logp.to('cpu').max(1)[1]
for batch_idx in range(batchsize):
if time_idx < out_len[batch_idx]:
alignments[batch_idx][-1].append(logp_vals[batch_idx])
del logp_vals
del logp
# If all samples predict / have predicted prior blanks, exit loop early
# This is equivalent to if single sample predicted k
if blank_mask.all():
not_blank = False
# If preserving alignments, convert the current Uj alignments into a torch.Tensor
# Then preserve U at current timestep Ti
# Finally, forward the timestep history to Ti+1 for that sample
# All of this should only be done iff the current time index <= sample-level AM length.
# Otherwise ignore and move to next sample / next timestep.
if self.preserve_alignments:
# convert Ti-th logits into a torch array
for batch_idx in range(batchsize):
# this checks if current timestep <= sample-level AM length
# If current timestep > sample-level AM length, no alignments will be added
# Therefore the list of Uj alignments is empty here.
if len(alignments[batch_idx][-1]) > 0:
alignments[batch_idx].append([]) # blank buffer for next timestep
else:
# Collect batch indices where blanks occurred now/past
blank_indices = []
if hidden is not None:
blank_indices = (blank_mask == 1).nonzero(as_tuple=False)
# Recover prior state for all samples which predicted blank now/past
if hidden is not None:
# LSTM has 2 states
for state_id in range(len(hidden)):
hidden_prime[state_id][:, blank_indices, :] = hidden[state_id][:, blank_indices, :]
# Recover prior predicted label for all samples which predicted blank now/past
k[blank_indices] = last_label[blank_indices, 0]
# Update new label and hidden state for next iteration
last_label = k.clone().view(-1, 1)
hidden = hidden_prime
# Update predicted labels, accounting for time mask
# If blank was predicted even once, now or in the past,
# Force the current predicted label to also be blank
# This ensures that blanks propogate across all timesteps
# once they have occured (normally stopping condition of sample level loop).
for kidx, ki in enumerate(k):
if blank_mask[kidx] == 0:
label[kidx].append(ki)
timesteps[kidx].append(time_idx)
symbols_added += 1
# Remove trailing empty list of alignments at T_{am-len} x Uj
if self.preserve_alignments:
for batch_idx in range(batchsize):
if len(alignments[batch_idx][-1]) == 0:
del alignments[batch_idx][-1]
return label, timesteps, alignments
@torch.no_grad()
def _greedy_decode_masked(self, x: torch.Tensor, out_len: torch.Tensor, device: torch.device):
# x: [B, T, D]
# out_len: [B]
# device: torch.device
# Initialize state
hidden = None
batchsize = x.shape[0]
# Output string buffer
label = [[] for _ in range(batchsize)]
timesteps = [[] for _ in range(batchsize)]
# If alignments need to be preserved, register a danling list to hold the values
if self.preserve_alignments:
# alignments is a 3-dimensional dangling list representing B x T x U
alignments = []
for _ in range(batchsize):
alignments.append([[]])
else:
alignments = None
# Last Label buffer + Last Label without blank buffer
# batch level equivalent of the last_label
last_label = torch.full([batchsize, 1], fill_value=self._blank_index, dtype=torch.long, device=device)
last_label_without_blank = last_label.clone()
# Mask buffers
blank_mask = torch.full([batchsize], fill_value=0, dtype=torch.bool, device=device)
# Get max sequence length
max_out_len = out_len.max()
for time_idx in range(max_out_len):
f = x.narrow(dim=1, start=time_idx, length=1) # [B, 1, D]
# Prepare t timestamp batch variables
not_blank = True
symbols_added = 0
# Reset blank mask
blank_mask.mul_(False)
# Update blank mask with time mask
# Batch: [B, T, D], but Bi may have seq len < max(seq_lens_in_batch)
# Forcibly mask with "blank" tokens, for all sample where current time step T > seq_len
blank_mask = time_idx >= out_len
# Start inner loop
while not_blank and (self.max_symbols is None or symbols_added < self.max_symbols):
# Batch prediction and joint network steps
# If very first prediction step, submit SOS tag (blank) to pred_step.
# This feeds a zero tensor as input to AbstractRNNTDecoder to prime the state
if time_idx == 0 and symbols_added == 0:
g, hidden_prime = self._pred_step(self._SOS, hidden, batch_size=batchsize)
else:
# Set a dummy label for the blank value
# This value will be overwritten by "blank" again the last label update below
# This is done as vocabulary of prediction network does not contain "blank" token of RNNT
last_label_without_blank_mask = last_label == self._blank_index
last_label_without_blank[last_label_without_blank_mask] = 0 # temp change of label
last_label_without_blank[~last_label_without_blank_mask] = last_label[
~last_label_without_blank_mask
]
# Perform batch step prediction of decoder, getting new states and scores ("g")
g, hidden_prime = self._pred_step(last_label_without_blank, hidden, batch_size=batchsize)
# Batched joint step - Output = [B, V + 1]
logp = self._joint_step(f, g, log_normalize=None)[:, 0, 0, :]
if logp.dtype != torch.float32:
logp = logp.float()
# Get index k, of max prob for batch
v, k = logp.max(1)
del v, g
# Update blank mask with current predicted blanks
# This is accumulating blanks over all time steps T and all target steps min(max_symbols, U)
k_is_blank = k == self._blank_index
blank_mask.bitwise_or_(k_is_blank)
# If preserving alignments, check if sequence length of sample has been reached
# before adding alignment
if self.preserve_alignments:
# Insert ids into last timestep per sample
logp_vals = logp.to('cpu').max(1)[1]
for batch_idx in range(batchsize):
if time_idx < out_len[batch_idx]:
alignments[batch_idx][-1].append(logp_vals[batch_idx])
del logp_vals
del logp
# If all samples predict / have predicted prior blanks, exit loop early
# This is equivalent to if single sample predicted k
if blank_mask.all():
not_blank = False
# If preserving alignments, convert the current Uj alignments into a torch.Tensor
# Then preserve U at current timestep Ti
# Finally, forward the timestep history to Ti+1 for that sample
# All of this should only be done iff the current time index <= sample-level AM length.
# Otherwise ignore and move to next sample / next timestep.
if self.preserve_alignments:
# convert Ti-th logits into a torch array
for batch_idx in range(batchsize):
# this checks if current timestep <= sample-level AM length
# If current timestep > sample-level AM length, no alignments will be added
# Therefore the list of Uj alignments is empty here.
if len(alignments[batch_idx][-1]) > 0:
alignments[batch_idx].append([]) # blank buffer for next timestep
else:
# Collect batch indices where blanks occurred now/past
blank_indices = []
if hidden is not None:
blank_indices = (blank_mask == 1).nonzero(as_tuple=False)
# Recover prior state for all samples which predicted blank now/past
if hidden is not None:
# LSTM has 2 states
for state_id in range(len(hidden)):
hidden_prime[state_id][:, blank_indices, :] = hidden[state_id][:, blank_indices, :]
# Recover prior predicted label for all samples which predicted blank now/past
k[blank_indices] = last_label[blank_indices, 0]
# Update new label and hidden state for next iteration
last_label = k.view(-1, 1)
hidden = hidden_prime
# Update predicted labels, accounting for time mask
# If blank was predicted even once, now or in the past,
# Force the current predicted label to also be blank
# This ensures that blanks propogate across all timesteps
# once they have occured (normally stopping condition of sample level loop).
for kidx, ki in enumerate(k):
if blank_mask[kidx] == 0:
label[kidx].append(ki)
timesteps[kidx].append(time_idx)
symbols_added += 1
# Remove trailing empty list of alignments at T_{am-len} x Uj
if self.preserve_alignments:
for batch_idx in range(batchsize):
if len(alignments[batch_idx][-1]) == 0:
del alignments[batch_idx][-1]
return label, timesteps, alignments
@dataclass
class GreedyRNNTInferConfig:
max_symbols_per_step: Optional[int] = None
preserve_alignments: bool = False
@dataclass
class GreedyBatchedRNNTInferConfig:
max_symbols_per_step: Optional[int] = None
preserve_alignments: bool = False
@@ -0,0 +1,152 @@
# Copyright (c) 2020, 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 random
import torch
import torch.nn as nn
from nemo.core.classes import Typing, typecheck
from nemo.core.neural_types import NeuralType, SpectrogramType
class SpecAugment(nn.Module, Typing):
"""
Zeroes out(cuts) random continuous horisontal or
vertical segments of the spectrogram as described in
SpecAugment (https://arxiv.org/abs/1904.08779).
params:
freq_masks - how many frequency segments should be cut
time_masks - how many time segments should be cut
freq_width - maximum number of frequencies to be cut in one segment
time_width - maximum number of time steps to be cut in one segment.
Can be a positive integer or a float value in the range [0, 1].
If positive integer value, defines maximum number of time steps
to be cut in one segment.
If a float value, defines maximum percentage of timesteps that
are cut adaptively.
"""
@property
def input_types(self):
"""Returns definitions of module input types
"""
return {"input_spec": NeuralType(('B', 'D', 'T'), SpectrogramType())}
@property
def output_types(self):
"""Returns definitions of module output types
"""
return {"augmented_spec": NeuralType(('B', 'D', 'T'), SpectrogramType())}
def __init__(
self, freq_masks=0, time_masks=0, freq_width=10, time_width=10, rng=None, mask_value=0.0,
):
super(SpecAugment, self).__init__()
self._rng = random.Random() if rng is None else rng
self.freq_masks = freq_masks
self.time_masks = time_masks
self.freq_width = freq_width
self.time_width = time_width
self.mask_value = mask_value
if isinstance(time_width, int):
self.adaptive_temporal_width = False
else:
if time_width > 1.0 or time_width < 0.0:
raise ValueError("If `time_width` is a float value, must be in range [0, 1]")
self.adaptive_temporal_width = True
@typecheck()
@torch.no_grad()
def forward(self, input_spec):
sh = input_spec.shape
if self.adaptive_temporal_width:
time_width = max(1, int(sh[2] * self.time_width))
else:
time_width = self.time_width
for idx in range(sh[0]):
for i in range(self.freq_masks):
x_left = self._rng.randint(0, sh[1] - self.freq_width)
w = self._rng.randint(0, self.freq_width)
input_spec[idx, x_left : x_left + w, :] = self.mask_value
for i in range(self.time_masks):
y_left = self._rng.randint(0, sh[2] - time_width)
w = self._rng.randint(0, time_width)
input_spec[idx, :, y_left : y_left + w] = self.mask_value
return input_spec
class SpecCutout(nn.Module, Typing):
"""
Zeroes out(cuts) random rectangles in the spectrogram
as described in (https://arxiv.org/abs/1708.04552).
params:
rect_masks - how many rectangular masks should be cut
rect_freq - maximum size of cut rectangles along the frequency dimension
rect_time - maximum size of cut rectangles along the time dimension
"""
@property
def input_types(self):
"""Returns definitions of module input types
"""
return {"input_spec": NeuralType(('B', 'D', 'T'), SpectrogramType())}
@property
def output_types(self):
"""Returns definitions of module output types
"""
return {"augmented_spec": NeuralType(('B', 'D', 'T'), SpectrogramType())}
def __init__(self, rect_masks=0, rect_time=5, rect_freq=20, rng=None):
super(SpecCutout, self).__init__()
self._rng = random.Random() if rng is None else rng
self.rect_masks = rect_masks
self.rect_time = rect_time
self.rect_freq = rect_freq
@typecheck()
@torch.no_grad()
def forward(self, input_spec):
sh = input_spec.shape
for idx in range(sh[0]):
for i in range(self.rect_masks):
rect_x = self._rng.randint(0, sh[1] - self.rect_freq)
rect_y = self._rng.randint(0, sh[2] - self.rect_time)
w_x = self._rng.randint(0, self.rect_freq)
w_y = self._rng.randint(0, self.rect_time)
input_spec[idx, rect_x : rect_x + w_x, rect_y : rect_y + w_y] = 0.0
return input_spec
@@ -0,0 +1,138 @@
# Copyright (c) 2020, 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 math
import torch
import torch.nn as nn
class ConvSubsampling(torch.nn.Module):
"""Convolutional subsampling which supports VGGNet and striding approach introduced in:
VGGNet Subsampling: https://arxiv.org/pdf/1910.12977.pdf
Striding Subsampling:
"Speech-Transformer: A No-Recurrence Sequence-to-Sequence Model for Speech Recognition" by Linhao Dong et al.
Args:
subsampling (str): The subsampling technique from {"vggnet", "striding"}
subsampling_factor (int): The subsampling factor which should be a power of 2
feat_in (int): size of the input features
feat_out (int): size of the output features
conv_channels (int): Number of channels for the convolution layers.
activation (Module): activation function, default is nn.ReLU()
"""
def __init__(self, subsampling, subsampling_factor, feat_in, feat_out, conv_channels, activation=nn.ReLU()):
super(ConvSubsampling, self).__init__()
self._subsampling = subsampling
if subsampling_factor % 2 != 0:
raise ValueError("Sampling factor should be a multiply of 2!")
self._sampling_num = int(math.log(subsampling_factor, 2))
in_channels = 1
layers = []
if subsampling == 'vggnet':
self._padding = 0
self._stride = 2
self._kernel_size = 2
self._ceil_mode = True
for i in range(self._sampling_num):
layers.append(
torch.nn.Conv2d(
in_channels=in_channels, out_channels=conv_channels, kernel_size=3, stride=1, padding=1
)
)
layers.append(activation)
layers.append(
torch.nn.Conv2d(
in_channels=conv_channels, out_channels=conv_channels, kernel_size=3, stride=1, padding=1
)
)
layers.append(activation)
layers.append(
torch.nn.MaxPool2d(
kernel_size=self._kernel_size,
stride=self._stride,
padding=self._padding,
ceil_mode=self._ceil_mode,
)
)
in_channels = conv_channels
elif subsampling == 'striding':
self._padding = 1
self._stride = 2
self._kernel_size = 3
self._ceil_mode = False
for i in range(self._sampling_num):
layers.append(
torch.nn.Conv2d(
in_channels=in_channels,
out_channels=conv_channels,
kernel_size=self._kernel_size,
stride=self._stride,
padding=self._padding,
)
)
layers.append(activation)
in_channels = conv_channels
else:
raise ValueError(f"Not valid sub-sampling: {subsampling}!")
in_length = feat_in
for i in range(self._sampling_num):
out_length = calc_length(
length=int(in_length),
padding=self._padding,
kernel_size=self._kernel_size,
stride=self._stride,
ceil_mode=self._ceil_mode,
)
in_length = out_length
self.out = torch.nn.Linear(conv_channels * out_length, feat_out)
self.conv = torch.nn.Sequential(*layers)
def forward(self, x, lengths):
x = x.unsqueeze(1)
x = self.conv(x)
b, c, t, f = x.size()
x = self.out(x.transpose(1, 2).contiguous().view(b, t, c * f))
# TODO: improve the performance of length calculation
new_lengths = lengths
for i in range(self._sampling_num):
new_lengths = [
calc_length(
length=int(length),
padding=self._padding,
kernel_size=self._kernel_size,
stride=self._stride,
ceil_mode=self._ceil_mode,
)
for length in new_lengths
]
new_lengths = torch.IntTensor(new_lengths).to(lengths.device)
return x, new_lengths
def calc_length(length, padding, kernel_size, stride, ceil_mode):
""" Calculates the output length of a Tensor passed through a convolution or max pooling layer"""
if ceil_mode:
length = math.ceil((length + (2 * padding) - (kernel_size - 1) - 1) / float(stride) + 1)
else:
length = math.floor((length + (2 * padding) - (kernel_size - 1) - 1) / float(stride) + 1)
return length
@@ -0,0 +1,222 @@
# Copyright (c) 2020, 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.
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import math
from typing import List, Tuple
import torch
from torch import nn
from nemo.collections.asr.models.wav2vec.wav2vec_config import Wav2VecConvExtractorMode, Wav2VecTransformerConfig
class TransposeLast(torch.nn.Module):
"""
Transposes last dimension. Useful for adding to a sequential block.
"""
def forward(self, x):
return x.transpose(-2, -1)
class SamePad(torch.nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.remove = kernel_size % 2 == 0
def forward(self, x):
if self.remove:
x = x[:, :, :-1]
return x
class ConvFeatureEncoder(nn.Module):
"""
Converts input raw audio into features for downstream transformer model.
Uses 1D convolutional blocks with GeLU activation.
"""
def __init__(
self,
conv_layers: List[Tuple[int, int, int]],
mode: Wav2VecConvExtractorMode = Wav2VecConvExtractorMode.default,
conv_bias: bool = False,
):
super().__init__()
def block(
n_in, n_out, k, stride, is_layer_norm=False, is_group_norm=False, conv_bias=False,
):
def make_conv():
conv = nn.Conv1d(n_in, n_out, k, stride=stride, bias=conv_bias)
nn.init.kaiming_normal_(conv.weight)
return conv
assert (is_layer_norm and is_group_norm) is False, "layer norm and group norm are exclusive"
if is_layer_norm:
return nn.Sequential(
make_conv(),
nn.Sequential(TransposeLast(), nn.LayerNorm(dim, elementwise_affine=True), TransposeLast()),
nn.GELU(),
)
elif is_group_norm:
return nn.Sequential(make_conv(), nn.GroupNorm(dim, dim, affine=True), nn.GELU(),)
else:
return nn.Sequential(make_conv(), nn.GELU())
in_d = 1
self.conv_layers = nn.ModuleList()
for i, cl in enumerate(conv_layers):
assert len(cl) == 3, "invalid conv definition: " + str(cl)
(dim, k, stride) = cl
self.conv_layers.append(
block(
in_d,
dim,
k,
stride,
is_layer_norm=mode is Wav2VecConvExtractorMode.layer_norm,
is_group_norm=mode is Wav2VecConvExtractorMode.default and i == 0,
conv_bias=conv_bias,
)
)
in_d = dim
def forward(self, x):
# BxT -> BxCxT
x = x.unsqueeze(1)
for conv in self.conv_layers:
x = conv(x)
return x
class Wav2VecTransformerEncoder(nn.Module):
def __init__(self, cfg: Wav2VecTransformerConfig):
super().__init__()
conv_cfg = cfg.conv
self.dropout = cfg.dropout
self.embedding_dim = cfg.encoder.embedding_dim
self.layer_norm_first = cfg.encoder.layer_norm_first
# positional convolutional embeddings
self.pos_conv = nn.Conv1d(
self.embedding_dim,
self.embedding_dim,
kernel_size=conv_cfg.conv_pos,
padding=conv_cfg.conv_pos // 2,
groups=conv_cfg.conv_pos_groups,
)
self.feature_dropout = nn.Dropout(self.dropout)
dropout = 0
std = math.sqrt((4 * (1.0 - dropout)) / (conv_cfg.conv_pos * self.embedding_dim))
nn.init.normal_(self.pos_conv.weight, mean=0, std=std)
nn.init.constant_(self.pos_conv.bias, 0)
self.pos_conv = nn.utils.weight_norm(self.pos_conv, name="weight", dim=2)
self.pos_conv = nn.Sequential(self.pos_conv, SamePad(conv_cfg.conv_pos), nn.GELU())
encoder_cfg = cfg.encoder
self.transformer_encoder = nn.TransformerEncoder(
encoder_layer=nn.TransformerEncoderLayer(
d_model=self.embedding_dim,
nhead=encoder_cfg.num_attention_heads,
dim_feedforward=encoder_cfg.ffn_embedding_dim,
dropout=self.dropout,
activation=encoder_cfg.activation_fn.value,
),
num_layers=encoder_cfg.encoder_layers,
)
self.layer_norm = nn.LayerNorm(self.embedding_dim)
self.apply(init_bert_params)
def forward(self, x, padding_mask=None):
x = self.extract_features(x, padding_mask)
if self.layer_norm_first:
x = self.layer_norm(x)
return x
def extract_features(self, x, padding_mask=None):
if padding_mask is not None:
x[padding_mask] = 0
x_conv = self.pos_conv(x.transpose(1, 2))
x_conv = x_conv.transpose(1, 2)
x += x_conv
if not self.layer_norm_first:
x = self.layer_norm(x)
x = self.feature_dropout(x)
# B x T x C -> T x B x C
x = x.transpose(0, 1)
x = self.transformer_encoder(x, src_key_padding_mask=padding_mask)
# T x B x C -> B x T x C
x = x.transpose(0, 1)
return x
class GradMultiply(torch.autograd.Function):
@staticmethod
def forward(ctx, x, scale):
ctx.scale = scale
res = x.new(x)
return res
@staticmethod
def backward(ctx, grad):
return grad * ctx.scale, None
def init_bert_params(module):
"""
Initialize the weights specific to the BERT Model.
This overrides the default initializations depending on the specified arguments.
1. If normal_init_linear_weights is set then weights of linear
layer will be initialized using the normal distribution and
bias will be set to the specified value.
2. If normal_init_embed_weights is set then weights of embedding
layer will be initialized using the normal distribution.
3. If normal_init_proj_weights is set then weights of
in_project_weight for MultiHeadAttention initialized using
the normal distribution (to be validated).
"""
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=0.02)
if module.bias is not None:
module.bias.data.zero_()
if isinstance(module, nn.Embedding):
module.weight.data.normal_(mean=0.0, std=0.02)
if module.padding_idx is not None:
module.weight.data[module.padding_idx].zero_()
if isinstance(module, nn.TransformerEncoderLayer):
module.self_attn.in_proj_weight.data.normal_(mean=0.0, std=0.02)
@@ -0,0 +1,13 @@
# Copyright (c) 2021, 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.
@@ -0,0 +1,24 @@
# Copyright (c) 2020, 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 torch.nn as nn
__all__ = ['Swish']
class Swish(nn.SiLU):
"""
Swish activation function introduced in 'https://arxiv.org/abs/1710.05941'
Mathematically identical to SiLU. See note in nn.SiLU for references.
"""
@@ -0,0 +1,82 @@
# Copyright (c) 2021, 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.
from typing import Optional
from omegaconf import DictConfig, open_dict
from nemo.collections.asr.modules import conv_asr
from nemo.collections.asr.parts.submodules import jasper
from nemo.utils import logging
def change_conv_asr_se_context_window(model: 'ASRModel', context_window: int, update_config: bool = True):
"""
Update the context window of the SqueezeExcitation module if the provided model contains an
`encoder` which is an instance of `ConvASREncoder`.
Args:
model: A subclass of `ASRModel`, itself a subclass of `ModelPT`.
context_window: An integer representing the number of input timeframes that will be used
to compute the context. Each timeframe corresponds to a single window stride of the
STFT features.
Say the window_stride = 0.01s, then a context window of 128 represents 128 * 0.01 s
of context to compute the Squeeze step.
update_config: Whether to update the config or not with the new context window.
"""
if update_config and not hasattr(model.cfg, 'encoder'):
logging.info(
"Could not change the context window in SqueezeExcite module "
"since the model provided does not contain an `encoder` module in its config."
)
return
if not isinstance(model.encoder, conv_asr.ConvASREncoder):
logging.info(
f"Could not change the context window in SqueezeExcite module "
f"since the `encoder` module is not an instance of `ConvASREncoder`.\n"
f"Provided encoder class = {model.encoder.__class__.__name__}"
)
return
enc_cfg = model.cfg.encoder if update_config else None
if enc_cfg is not None:
with open_dict(enc_cfg):
_update_se_context_window(model, context_window, cfg=enc_cfg)
else:
_update_se_context_window(model, context_window)
# Update model config
if update_config:
model.cfg.encoder = enc_cfg
def _update_se_context_window(model: 'ASRModel', context_window: int, cfg: Optional[DictConfig] = None):
jasper_block_counter = -1
for name, m in model.named_modules():
if type(m) == jasper.JasperBlock:
jasper_block_counter += 1
if type(m) == jasper.MaskedConv1d:
if m.conv.stride[0] > 1 and 'mconv' in name:
context_window = context_window // m.conv.stride[0]
if type(m) == jasper.SqueezeExcite:
m.change_context_window(context_window=context_window)
# update config
if cfg is not None:
cfg.jasper[jasper_block_counter].se_context_size = context_window
@@ -0,0 +1,271 @@
# Copyright (c) 2021, 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.
# Copyright (c) 2007-2020 The scikit-learn developers.
# BSD 3-Clause License
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# This file is part of https://github.com/scikit-learn/scikit-learn/blob/114616d9f6ce9eba7c1aacd3d4a254f868010e25/sklearn/manifold/_spectral_embedding.py and
# https://github.com/tango4j/Auto-Tuning-Spectral-Clustering.
import numpy as np
import scipy
from scipy import sparse
from sklearn.cluster import SpectralClustering as sklearn_SpectralClustering
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(0, 1))
def get_kneighbors_conn(X_dist, p_neighbors):
X_dist_out = np.zeros_like(X_dist)
for i, line in enumerate(X_dist):
sorted_idx = np.argsort(line)
sorted_idx = sorted_idx[::-1]
indices = sorted_idx[:p_neighbors]
X_dist_out[indices, i] = 1
return X_dist_out
def get_X_conn_from_dist(X_dist_raw, p_neighbors):
X_r = get_kneighbors_conn(X_dist_raw, p_neighbors)
X_conn_from_dist = 0.5 * (X_r + X_r.T)
return X_conn_from_dist
def isFullyConnected(X_conn_from_dist):
""" Return whether the graph is connected (True) or Not (False).
Parameters
----------
graph : {array-like, sparse matrix} of shape (n_samples, n_samples)
Adjacency matrix of the graph, non-zero weight means an edge
between the nodes.
Returns
-------
is_connected : bool
True means the graph is fully connected and False means not.
"""
gC = _graph_connected_component(X_conn_from_dist, 0).sum() == X_conn_from_dist.shape[0]
return gC
def gc_thres_min_gc(mat, max_n, n_list):
p_neighbors, index = 1, 0
X_conn_from_dist = get_X_conn_from_dist(mat, p_neighbors)
fully_connected = isFullyConnected(X_conn_from_dist)
for i, p_neighbors in enumerate(n_list):
fully_connected = isFullyConnected(X_conn_from_dist)
X_conn_from_dist = get_X_conn_from_dist(mat, p_neighbors)
if fully_connected or p_neighbors > max_n:
break
return X_conn_from_dist, p_neighbors
def _graph_connected_component(graph, node_id):
"""Find the largest graph connected components that contains one
given node.
Parameters
----------
graph : array-like of shape (n_samples, n_samples)
Adjacency matrix of the graph, non-zero weight means an edge
between the nodes.
node_id : int
The index of the query node of the graph.
Returns
-------
connected_components_matrix : array-like of shape (n_samples,)
An array of bool value indicating the indexes of the nodes
belonging to the largest connected components of the given query
node.
"""
n_node = graph.shape[0]
if sparse.issparse(graph):
graph = graph.tocsr()
connected_nodes = np.zeros(n_node, dtype=np.bool)
nodes_to_explore = np.zeros(n_node, dtype=np.bool)
nodes_to_explore[node_id] = True
for _ in range(n_node):
last_num_component = connected_nodes.sum()
np.logical_or(connected_nodes, nodes_to_explore, out=connected_nodes)
if last_num_component >= connected_nodes.sum():
break
indices = np.where(nodes_to_explore)[0]
nodes_to_explore.fill(False)
for i in indices:
if sparse.issparse(graph):
neighbors = graph[i].toarray().ravel()
else:
neighbors = graph[i]
np.logical_or(nodes_to_explore, neighbors, out=nodes_to_explore)
return connected_nodes
def getLaplacian(X):
"""
returns laplacian matrix of X
"""
X[np.diag_indices(X.shape[0])] = 0
A = X
D = np.sum(np.abs(A), axis=1)
D = np.diag(D)
L = D - A
return L
def eig_decompose(L, k):
try:
lambdas, eig_vecs = scipy.linalg.eigh(L)
except:
try:
lambdas = scipy.linalg.eigvals(L)
eig_vecs = None
except:
lambdas, eig_vecs = scipy.sparse.linalg.eigsh(L) ### Inaccurate results
return lambdas, eig_vecs
def getLamdaGaplist(lambdas):
lambda_gap_list = []
for i in range(len(lambdas) - 1):
lambda_gap_list.append(float(lambdas[i + 1]) - float(lambdas[i]))
return lambda_gap_list
def estimate_num_of_spkrs(X_conn, SPK_MAX):
"""
Estimate number of speakers using eigen decompose on Laplacian Matrix
"""
L = getLaplacian(X_conn)
lambdas, eig_vals = eig_decompose(L, k=X_conn.shape[0])
lambdas = np.sort(lambdas)
lambda_gap_list = getLamdaGaplist(lambdas)
num_of_spk = np.argmax(lambda_gap_list[: min(SPK_MAX, len(lambda_gap_list))]) + 1
return num_of_spk, lambdas, lambda_gap_list
def NMEanalysis(mat, SPK_MAX, max_rp_threshold=0.250, sparse_search=True, search_p_volume=20, fixed_thres=None):
"""
Normalized Eigengap analysis to get p_neighbours and estimated num of speakers
Reference: https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=8937487&tag=1
"""
eps = 1e-10
eig_ratio_list = []
if fixed_thres:
p_neighbors_list = [int(mat.shape[0] * fixed_thres)]
max_N = p_neighbors_list[0]
else:
max_N = int(mat.shape[0] * max_rp_threshold)
if sparse_search:
N = min(max_N, search_p_volume)
p_neighbors_list = list(np.linspace(1, max_N, N, endpoint=True).astype(int))
else:
p_neighbors_list = list(range(1, max_N))
est_spk_n_dict = {}
for p_neighbors in p_neighbors_list:
X_conn_from_dist = get_X_conn_from_dist(mat, p_neighbors)
est_num_of_spk, lambdas, lambda_gap_list = estimate_num_of_spkrs(X_conn_from_dist, SPK_MAX)
est_spk_n_dict[p_neighbors] = (est_num_of_spk, lambdas)
arg_sorted_idx = np.argsort(lambda_gap_list[:SPK_MAX])[::-1]
max_key = arg_sorted_idx[0]
max_eig_gap = lambda_gap_list[max_key] / (max(lambdas) + eps)
eig_ratio_value = (p_neighbors / mat.shape[0]) / (max_eig_gap + eps)
eig_ratio_list.append(eig_ratio_value)
index_nn = np.argmin(eig_ratio_list)
rp_p_neighbors = p_neighbors_list[index_nn]
X_conn_from_dist = get_X_conn_from_dist(mat, rp_p_neighbors)
if not isFullyConnected(X_conn_from_dist):
X_conn_from_dist, rp_p_neighbors = gc_thres_min_gc(mat, max_N, p_neighbors_list)
return (
X_conn_from_dist,
float(rp_p_neighbors / mat.shape[0]),
est_spk_n_dict[rp_p_neighbors][0],
est_spk_n_dict[rp_p_neighbors][1],
rp_p_neighbors,
)
def get_eigen_matrix(emb):
"""
input
emb: embedding
output
similarity matrix
"""
sim_d = cosine_similarity(emb)
scaler.fit(sim_d)
sim_d = scaler.transform(sim_d)
return sim_d
def COSclustering(key, emb, oracle_num_speakers=None, max_num_speaker=8, MIN_SAMPLES=6):
"""
input:
key (str): speaker uniq name
emb (np array): speaker embedding
oracle_num_speaker (int or None): oracle number of speakers if known else None
max_num_speakers (int): maximum number of clusters to consider for each session
MIN_SAMPLES (int): minimum number of samples required for NME clustering, this avoids
zero p_neighbour_lists. Default of 6 is selected since (1/rp_threshold) >= 4.
output:
Y (List[int]): speaker labels
"""
est_num_spks_out_list = []
mat = get_eigen_matrix(emb)
if oracle_num_speakers:
max_num_speaker = oracle_num_speakers
X_conn_spkcount, rp_thres_spkcount, est_num_of_spk, lambdas, p_neigh = NMEanalysis(mat, max_num_speaker)
if emb.shape[0] > MIN_SAMPLES:
X_conn_from_dist = get_X_conn_from_dist(mat, p_neigh)
else:
X_conn_from_dist = mat
if oracle_num_speakers:
est_num_of_spk = oracle_num_speakers
est_num_spks_out_list.append([key, str(est_num_of_spk)])
# Perform spectral clustering
spectral_model = sklearn_SpectralClustering(
affinity='precomputed',
eigen_solver='amg',
random_state=0,
n_jobs=-1,
n_clusters=est_num_of_spk,
eigen_tol=1e-10,
)
Y = spectral_model.fit_predict(X_conn_from_dist)
return Y
@@ -0,0 +1,88 @@
# Copyright (c) 2020, 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 numpy as np
from numba import jit
def phase_vocoder(D: np.ndarray, rate: float, phi_advance: np.ndarray, scale_buffer: np.ndarray):
"""
Optimized implementation of phase vocoder from Librosa.
Reference implementation:
- https://librosa.github.io/librosa/generated/librosa.core.phase_vocoder.html
Args:
D: Complex spectograms of shape [d, t, complex=2].
rate: Speed rate, must be float greater than 0.
phi_advance: Precomputed phase advance buffer array of length [n_fft + 1]
scale_buffer: Precomputed numpy buffer array of length [n_fft + 1]
Returns:
Complex64 ndarray of shape [d, t / rate, complex=2]
"""
time_steps = np.arange(0, D.shape[1], rate, dtype=np.float)
# Create an empty output array
d_stretch = np.zeros((D.shape[0], len(time_steps)), D.dtype, order='F')
# Phase accumulator; initialize to the first sample
phase_acc = np.angle(D[:, 0])
# Pad 0 columns to simplify boundary logic
D = np.pad(D, [(0, 0), (0, 2)], mode='constant')
d_stretch = _phase_vocoder_kernel(D, time_steps, phi_advance, d_stretch, phase_acc, scale_buffer)
return d_stretch
@jit(nopython=True, nogil=True)
def _phase_vocoder_kernel(D, time_steps, phi_advance, d_stretch, phase_acc, scale_buffer):
"""
Numba optimized kernel to compute the phase vocoder step.
Args:
D: Complex spectograms of shape [d, t, complex=2].
rate: Speed rate, must be float greater than 0.
time_steps: Numpy ndarray of linearly spaced time steps, shape = [t]
phi_advance: Precomputed phase advance buffer array of length [n_fft + 1]
d_stretch: Output complex matrix of shape [d, t / rate, complex=2]
phase_acc: Phase accumulator initialized to first sample of shape [d, complex=2]
scale_buffer: Precomputed numpy buffer array of length [n_fft + 1]
Returns:
Complex64 ndarray of shape [d, t / rate, complex=2]
"""
two_pi = 2.0 * np.pi
for (t, step) in enumerate(time_steps):
columns = D[:, int(step) : int(step + 2)]
columns_0 = columns[:, 0]
columns_1 = columns[:, 1]
# Weighting for linear magnitude interpolation
alpha = np.mod(step, 1.0)
mag = (1.0 - alpha) * np.abs(columns_0) + alpha * np.abs(columns_1)
# Store to output array
d_stretch[:, t] = mag * np.exp(1.0j * phase_acc)
# Compute phase advance
dphase = np.angle(columns_1) - np.angle(columns_0) - phi_advance
# Wrap to -pi:pi range
scale = dphase / two_pi
np.round(scale, 0, scale_buffer)
dphase = dphase - two_pi * scale_buffer
# Accumulate phase
phase_acc += phi_advance + dphase
return d_stretch
@@ -0,0 +1,89 @@
# Copyright (c) 2020, 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.
# Copyright 2017 Johns Hopkins University (Shinji Watanabe)
#
# 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.
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
import torch
@dataclass
class Hypothesis:
"""Hypothesis class for beam search algorithms.
score: A float score obtained from an AbstractRNNTDecoder module's score_hypothesis method.
y_sequence: Either a sequence of integer ids pointing to some vocabulary, or a packed torch.Tensor
behaving in the same manner. dtype must be torch.Long in the latter case.
dec_state: A list (or list of list) of LSTM-RNN decoder states. Can be None.
text: (Optional) A decoded string after processing via CTC / RNN-T decoding (removing the CTC/RNNT
`blank` tokens, and optionally merging word-pieces). Should be used as decoded string for
Word Error Rate calculation.
timestep: (Optional) A list of integer indices representing at which index in the decoding
process did the token appear. Should be of same length as the number of non-blank tokens.
alignments: (Optional) Represents the CTC / RNNT token alignments as integer tokens along an axis of
time T (for CTC) or Time x Target (TxU).
For CTC, represented as a single list of integer indices.
For RNNT, represented as a dangling list of list of integer indices.
Outer list represents Time dimension (T), inner list represents Target dimension (U).
The set of valid indices **includes** the CTC / RNNT blank token in order to represent alignments.
length: Represents the length of the sequence (the original length without padding), otherwise
defaults to 0.
y: (Unused) A list of torch.Tensors representing the list of hypotheses.
lm_state: (Unused) A dictionary state cache used by an external Language Model.
lm_scores: (Unused) Score of the external Language Model.
tokens: (Optional) A list of decoded tokens (can be characters or word-pieces.
"""
score: float
y_sequence: Union[List[int], torch.Tensor]
dec_state: Optional[Union[List[List[torch.Tensor]], List[torch.Tensor]]] = None
text: Optional[str] = None
timestep: Union[List[int], torch.Tensor] = field(default_factory=list)
alignments: Optional[Union[List[int], List[List[int]]]] = None
length: int = 0
y: List[torch.tensor] = None
lm_state: Union[Dict[str, Any], List[Any]] = None
lm_scores: torch.Tensor = None
tokens: Optional[Union[List[int], torch.Tensor]] = None
@dataclass
class NBestHypotheses:
"""List of N best hypotheses"""
n_best_hypotheses: Optional[List[Hypothesis]]
@@ -0,0 +1,400 @@
# Copyright (c) 2020, 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 json
import math
import os
import pickle as pkl
from copy import deepcopy
import numpy as np
from omegaconf import ListConfig
from pyannote.core import Annotation, Segment
from pyannote.metrics.diarization import DiarizationErrorRate
from tqdm import tqdm
from nemo.collections.asr.parts.utils.nmse_clustering import COSclustering
from nemo.utils import logging
"""
This file contains all the utility functions required for speaker embeddings part in diarization scripts
"""
def audio_rttm_map(audio_file_list, rttm_file_list=None):
"""
Returns a AUDIO_TO_RTTM dictionary thats maps all the unique file names with
audio file paths and corresponding ground truth rttm files calculated from audio
file list and rttm file list
Args:
audio_file_list(list,str): either list of audio file paths or file containing paths to audio files (required)
rttm_file_list(lisr,str): either list of rttm file paths or file containing paths to rttm files (optional)
[Required if DER needs to be calculated]
Returns:
AUDIO_RTTM_MAP (dict): dictionary thats maps all the unique file names with
audio file paths and corresponding ground truth rttm files
"""
rttm_notfound = False
if type(audio_file_list) in [list, ListConfig]:
audio_files = audio_file_list
else:
audio_pointer = open(audio_file_list, 'r')
audio_files = audio_pointer.read().splitlines()
if rttm_file_list:
if type(rttm_file_list) in [list, ListConfig]:
rttm_files = rttm_file_list
else:
rttm_pointer = open(rttm_file_list, 'r')
rttm_files = rttm_pointer.read().splitlines()
else:
rttm_notfound = True
rttm_files = ['-'] * len(audio_files)
assert len(audio_files) == len(rttm_files)
AUDIO_RTTM_MAP = {}
rttm_dict = {}
audio_dict = {}
for audio_file, rttm_file in zip(audio_files, rttm_files):
uniq_audio_name = audio_file.split('/')[-1].rsplit('.', 1)[0]
uniq_rttm_name = rttm_file.split('/')[-1].rsplit('.', 1)[0]
if rttm_notfound:
uniq_rttm_name = uniq_audio_name
audio_dict[uniq_audio_name] = audio_file
rttm_dict[uniq_rttm_name] = rttm_file
for key, value in audio_dict.items():
AUDIO_RTTM_MAP[key] = {'audio_path': audio_dict[key], 'rttm_path': rttm_dict[key]}
assert len(rttm_dict.items()) == len(audio_dict.items())
return AUDIO_RTTM_MAP
def get_contiguous_stamps(stamps):
"""
Return contiguous time stamps
"""
lines = deepcopy(stamps)
contiguous_stamps = []
for i in range(len(lines) - 1):
start, end, speaker = lines[i].split()
next_start, next_end, next_speaker = lines[i + 1].split()
if float(end) > float(next_start):
avg = str((float(next_start) + float(end)) / 2.0)
lines[i + 1] = ' '.join([avg, next_end, next_speaker])
contiguous_stamps.append(start + " " + avg + " " + speaker)
else:
contiguous_stamps.append(start + " " + end + " " + speaker)
start, end, speaker = lines[-1].split()
contiguous_stamps.append(start + " " + end + " " + speaker)
return contiguous_stamps
def merge_stamps(lines):
"""
merge time stamps of same speaker
"""
stamps = deepcopy(lines)
overlap_stamps = []
for i in range(len(stamps) - 1):
start, end, speaker = stamps[i].split()
next_start, next_end, next_speaker = stamps[i + 1].split()
if float(end) == float(next_start) and speaker == next_speaker:
stamps[i + 1] = ' '.join([start, next_end, next_speaker])
else:
overlap_stamps.append(start + " " + end + " " + speaker)
start, end, speaker = stamps[-1].split()
overlap_stamps.append(start + " " + end + " " + speaker)
return overlap_stamps
def labels_to_pyannote_object(labels):
"""
converts labels to pyannote object to calculate DER and for visualization
"""
annotation = Annotation()
for label in labels:
start, end, speaker = label.strip().split()
start, end = float(start), float(end)
annotation[Segment(start, end)] = speaker
return annotation
def labels_to_rttmfile(labels, uniq_id, out_rttm_dir):
"""
write rttm file with uniq_id name in out_rttm_dir with time_stamps in labels
"""
filename = os.path.join(out_rttm_dir, uniq_id + '.rttm')
with open(filename, 'w') as f:
for line in labels:
line = line.strip()
start, end, speaker = line.split()
duration = float(end) - float(start)
start = float(start)
log = 'SPEAKER {} 1 {:.3f} {:.3f} <NA> <NA> {} <NA> <NA>\n'.format(uniq_id, start, duration, speaker)
f.write(log)
def rttm_to_labels(rttm_filename):
"""
prepares time stamps label list from rttm file
"""
labels = []
with open(rttm_filename, 'r') as f:
for line in f.readlines():
rttm = line.strip().split()
start, end, speaker = float(rttm[3]), float(rttm[4]) + float(rttm[3]), rttm[7]
labels.append('{} {} {}'.format(start, end, speaker))
return labels
def get_time_stamps(embeddings_file, reco2num, manifest_path, sample_rate, window, shift):
"""
Loads embedding file and generates time stamps based on window and shift for speaker embeddings
clustering.
Args:
embeddings_file (str): Path to embeddings pickle file
reco2num (int,str): common integer number of speakers for every recording or path to
file that states unique_file id with number of speakers
manifest_path (str): path to manifest for time stamps matching
sample_rate (int): sample rate
window (float): window length in sec for speaker embeddings extraction
shift (float): shift length in sec for speaker embeddings extraction window shift
Returns:
embeddings (dict): Embeddings with key as unique_id
time_stamps (dict): time stamps list for each audio recording
speakers (dict): number of speaker for each audio recording
"""
embeddings = pkl.load(open(embeddings_file, 'rb'))
all_uniq_files = list(embeddings.keys())
num_files = len(all_uniq_files)
logging.info("Number of files to diarize: {}".format(num_files))
sample = all_uniq_files[0]
logging.info("sample '{}' embeddings shape is {}\n".format(sample, embeddings[sample][0].shape))
speakers = {}
if isinstance(reco2num, int) or reco2num is None:
for key in embeddings.keys():
speakers[key] = reco2num
elif isinstance(reco2num, str) and os.path.exists(reco2num):
for key in open(reco2num).readlines():
key = key.strip()
wav_id, num = key.split()
speakers[wav_id] = int(num)
else:
raise TypeError("reco2num must be None, int or path to file containing number of speakers for each uniq id")
with open(manifest_path, 'r') as manifest:
time_stamps = {}
for line in manifest.readlines():
line = line.strip()
line = json.loads(line)
audio, offset, duration = line['audio_filepath'], line['offset'], line['duration']
audio = os.path.basename(audio).rsplit('.', 1)[0]
if audio not in time_stamps:
time_stamps[audio] = []
start = offset
slice_end = start + duration
base = math.ceil((duration - window) / shift)
slices = 1 if base < 0 else base + 1
for slice_id in range(slices):
end = start + window
if end > slice_end:
end = slice_end
stamp = '{:.3f} {:.3f} '.format(start, end)
time_stamps[audio].append(stamp)
start = offset + (slice_id + 1) * shift
return embeddings, time_stamps, speakers
def perform_clustering(embeddings, time_stamps, speakers, audio_rttm_map, out_rttm_dir, max_num_speakers=8):
"""
performs spectral clustering on embeddings with time stamps generated from VAD output
Args:
embeddings (dict): Embeddings with key as unique_id
time_stamps (dict): time stamps list for each audio recording
speakers (dict): number of speaker for each audio recording
audio_rttm_map (dict): AUDIO_RTTM_MAP for mapping unique id with audio file path and rttm path
out_rttm_dir (str): Path to write predicted rttms
max_num_speakers (int): maximum number of speakers to consider for spectral clustering. Will be ignored if speakers['key'] is not None
Returns:
all_reference (list[Annotation]): reference annotations for score calculation
all_hypothesis (list[Annotation]): hypothesis annotations for score calculation
"""
all_hypothesis = []
all_reference = []
no_references = False
for uniq_key in tqdm(embeddings.keys()):
NUM_speakers = speakers[uniq_key]
emb = embeddings[uniq_key]
emb = np.asarray(emb)
cluster_labels = COSclustering(
uniq_key, emb, oracle_num_speakers=NUM_speakers, max_num_speaker=max_num_speakers
)
lines = time_stamps[uniq_key]
assert len(cluster_labels) == len(lines)
for idx, label in enumerate(cluster_labels):
tag = 'speaker_' + str(label)
lines[idx] += tag
a = get_contiguous_stamps(lines)
labels = merge_stamps(a)
if out_rttm_dir:
labels_to_rttmfile(labels, uniq_key, out_rttm_dir)
hypothesis = labels_to_pyannote_object(labels)
all_hypothesis.append(hypothesis)
rttm_file = audio_rttm_map[uniq_key]['rttm_path']
if os.path.exists(rttm_file) and not no_references:
ref_labels = rttm_to_labels(rttm_file)
reference = labels_to_pyannote_object(ref_labels)
all_reference.append(reference)
else:
no_references = True
all_reference = []
return all_reference, all_hypothesis
def get_DER(all_reference, all_hypothesis):
"""
calculates DER, CER, FA and MISS
Args:
all_reference (list[Annotation]): reference annotations for score calculation
all_hypothesis (list[Annotation]): hypothesis annotations for score calculation
Returns:
DER (float): Diarization Error Rate
CER (float): Confusion Error Rate
FA (float): False Alarm
Miss (float): Miss Detection
"""
metric = DiarizationErrorRate(collar=0.25, skip_overlap=True)
DER = 0
for reference, hypothesis in zip(all_reference, all_hypothesis):
metric(reference, hypothesis, detailed=True)
DER = abs(metric)
CER = metric['confusion'] / metric['total']
FA = metric['false alarm'] / metric['total']
MISS = metric['missed detection'] / metric['total']
metric.reset()
return DER, CER, FA, MISS
def perform_diarization(
embeddings_file=None,
reco2num=2,
manifest_path=None,
sample_rate=16000,
window=1.5,
shift=0.75,
audio_rttm_map=None,
out_rttm_dir=None,
max_num_speakers=8,
):
"""
Performs diarization with embeddings generated based on VAD time stamps with recording 2 num of speakers (reco2num)
for spectral clustering
"""
embeddings, time_stamps, speakers = get_time_stamps(
embeddings_file, reco2num, manifest_path, sample_rate, window, shift
)
logging.info("Performing Clustering")
all_reference, all_hypothesis = perform_clustering(
embeddings, time_stamps, speakers, audio_rttm_map, out_rttm_dir, max_num_speakers
)
if len(all_reference) and len(all_hypothesis):
DER, CER, FA, MISS = get_DER(all_reference, all_hypothesis)
logging.info(
"Cumulative results of all the files: \n FA: {:.3f}\t MISS {:.3f}\t \
Diarization ER: {:.3f}\t, Confusion ER:{:.3f}".format(
FA, MISS, DER, CER
)
)
else:
logging.warning("Please check if each ground truth RTTMs was present in provided path2groundtruth_rttm_files")
logging.warning("Skipping calculation of Diariazation Error Rate")
def write_rttm2manifest(paths2audio_files, paths2rttm_files, manifest_file):
"""
writes manifest file based on rttm files (or vad table out files). This manifest file would be used by
speaker diarizer to compute embeddings and cluster them. This function also takes care of overlap time stamps
Args:
audio_file_list(list,str): either list of audio file paths or file containing paths to audio files (required)
rttm_file_list(lisr,str): either list of rttm file paths or file containing paths to rttm files (optional)
manifest (str): path to write manifest file
Returns:
manifest (str): path to write manifest file
"""
AUDIO_RTTM_MAP = audio_rttm_map(paths2audio_files, paths2rttm_files)
with open(manifest_file, 'w') as outfile:
for key in AUDIO_RTTM_MAP:
f = open(AUDIO_RTTM_MAP[key]['rttm_path'], 'r')
audio_path = AUDIO_RTTM_MAP[key]['audio_path']
lines = f.readlines()
time_tup = (-1, -1)
for line in lines:
vad_out = line.strip().split()
if len(vad_out) > 3:
start, dur, activity = float(vad_out[3]), float(vad_out[4]), vad_out[7]
else:
start, dur, activity = float(vad_out[0]), float(vad_out[1]), vad_out[2]
start, dur = float("{:.3f}".format(start)), float("{:.3f}".format(dur))
if time_tup[0] >= 0 and start > time_tup[1]:
dur2 = float("{:.3f}".format(time_tup[1] - time_tup[0]))
meta = {"audio_filepath": audio_path, "offset": time_tup[0], "duration": dur2, "label": 'UNK'}
json.dump(meta, outfile)
outfile.write("\n")
time_tup = (start, start + dur)
else:
if time_tup[0] == -1:
time_tup = (start, start + dur)
else:
time_tup = (min(time_tup[0], start), max(time_tup[1], start + dur))
dur2 = float("{:.3f}".format(time_tup[1] - time_tup[0]))
meta = {"audio_filepath": audio_path, "offset": time_tup[0], "duration": dur2, "label": 'UNK'}
json.dump(meta, outfile)
outfile.write("\n")
f.close()
return manifest_file
@@ -0,0 +1,544 @@
# Copyright (c) 2020, 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 glob
import json
import os
from itertools import repeat
from multiprocessing import Pool
import librosa
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pyannote.core import Annotation, Segment
from pyannote.metrics import detection
from nemo.utils import logging
"""
This file contains all the utility functions required for speaker embeddings part in diarization scripts
"""
def prepare_manifest(config):
"""
Perform VAD on long audio snippet might cause CUDA out of memory issue.
Automatically split manifest entry by split_duration to avoid the potential memory issue.
"""
manifest_vad_input = config.get('manifest_vad_input', "manifest_vad_input.json")
input_audios = []
with open(config['manifest_filepath'], 'r') as manifest:
for line in manifest.readlines():
input_audios.append(json.loads(line.strip()))
p = Pool(processes=config['num_workers'])
args_func = {
'label': 'infer',
'split_duration': config['split_duration'],
'time_length': config['time_length'],
}
results = p.starmap(write_vad_infer_manifest, zip(input_audios, repeat(args_func)))
p.close()
if os.path.exists(manifest_vad_input):
logging.info("The prepared manifest file exists. Overwriting!")
os.remove(manifest_vad_input)
with open(manifest_vad_input, 'a') as fout:
for res in results:
for r in res:
json.dump(r, fout)
fout.write('\n')
fout.flush()
return manifest_vad_input
def write_vad_infer_manifest(file, args_func):
"""
Used by prepare_manifest.
Given a list of files, write them to manifest for dataloader with restrictions.
Args:
files (json) : file to be processed
label (str): label for audio snippet.
split_duration (float): max duration of each audio clip (each line in json)
time_length (float) : length of window for generating the frame. Used for taking care of joint.
Returns:
res (list) : list of generated metadata line of json for file
"""
res = []
label = args_func['label']
split_duration = args_func['split_duration']
time_length = args_func['time_length']
filepath = file['audio_filepath']
in_duration = file['duration']
in_offset = file['offset']
try:
sr = 16000
x, _sr = librosa.load(filepath, sr=sr, offset=in_offset, duration=in_duration)
duration = librosa.get_duration(x, sr=sr)
left = duration
current_offset = in_offset
status = 'single'
while left > 0:
if left <= split_duration:
status = 'end'
write_duration = left + time_length
current_offset -= time_length
offset_inc = left
left = 0
else:
if status == 'start' or status == 'next':
status = 'next'
else:
status = 'start'
if status == 'start':
write_duration = split_duration
offset_inc = split_duration
else:
write_duration = split_duration + time_length
current_offset -= time_length
offset_inc = split_duration + time_length
left -= split_duration
metadata = {
'audio_filepath': filepath,
'duration': write_duration,
'label': label,
'text': '_',
'offset': current_offset,
}
res.append(metadata)
current_offset += offset_inc
except Exception as e:
err_file = "error.log"
with open(err_file, 'w') as fout:
fout.write(file + ":" + str(e))
return res
def get_vad_stream_status(data):
"""
Generate a list of status for each snippet in manifest. A snippet should be in single, start, next or end status.
Used for concatenate to full audio file.
Args:
data (list): list of filepath of audio snippet
Returns:
status (list): list of status of each snippet.
"""
if len(data) == 1:
return ['single']
status = [None] * len(data)
for i in range(len(data)):
if i == 0:
status[i] = 'start' if data[i] == data[i + 1] else 'single'
elif i == len(data) - 1:
status[i] = 'end' if data[i] == data[i - 1] else 'single'
else:
if data[i] != data[i - 1] and data[i] == data[i + 1]:
status[i] = 'start'
elif data[i] == data[i - 1] and data[i] == data[i + 1]:
status[i] = 'next'
elif data[i] == data[i - 1] and data[i] != data[i + 1]:
status[i] = 'end'
else:
status[i] = 'single'
return status
def generate_overlap_vad_seq(frame_pred_dir, smoothing_method, overlap, seg_len, shift_len, num_workers):
"""
Gnerate predictions with overlapping input windows/segments. Then a smoothing filter is applied to decide the label for a frame spanned by multiple windows.
Two common smoothing filters are supported: majority vote (median) and average (mean).
Args:
frame_pred_dir (str): Directory of frame prediction file to be processed.
smoothing_method (str): median or mean smoothing filter.
overlap (float): amounts of overlap of adjacent windows.
seg_len (float): length of window for generating the frame.
shift_len (float): amount of shift of window for generating the frame.
out_dir (str): directory of generated predictions.
num_workers(float): number of process for multiprocessing
Returns:
overlap_out_dir(str): directory of generate predictions.
"""
p = Pool(processes=num_workers)
frame_filepathlist = glob.glob(frame_pred_dir + "/*.frame")
overlap_out_dir = frame_pred_dir + "/overlap_smoothing_output" + "_" + smoothing_method + "_" + str(overlap)
if not os.path.exists(overlap_out_dir):
os.mkdir(overlap_out_dir)
per_args = {
"out_dir": overlap_out_dir,
"smoothing_method": smoothing_method,
"overlap": overlap,
"seg_len": seg_len,
"shift_len": shift_len,
}
p.starmap(generate_overlap_vad_seq_per_file, zip(frame_filepathlist, repeat(per_args)))
p.close()
p.join()
return overlap_out_dir
def generate_overlap_vad_seq_per_file(frame_filepath, per_args):
"""
Use generated frame prediction (generated by shifting window of shift_len (10ms)) to generate prediction with overlapping input window/segments
See discription in generate_overlap_vad_seq.
"""
try:
smoothing_method = per_args['smoothing_method']
overlap = per_args['overlap']
seg_len = per_args['seg_len']
shift_len = per_args['shift_len']
out_dir = per_args['out_dir']
frame = np.loadtxt(frame_filepath)
name = os.path.basename(frame_filepath).split(".frame")[0] + "." + smoothing_method
overlap_filepath = os.path.join(out_dir, name)
shift = int(shift_len / 0.01) # number of units of shift
seg = int((seg_len / 0.01 + 1)) # number of units of each window/segment
jump_on_target = int(seg * (1 - overlap)) # jump on target generated sequence
jump_on_frame = int(jump_on_target / shift) # jump on input frame sequence
if jump_on_frame < 1:
raise ValueError(
f"Note we jump over frame sequence to generate overlapping input segments. \n \
Your input makes jump_on_fram={jump_on_frame} < 1 which is invalid because it cannot jump and will stuck.\n \
Please try different seg_len, shift_len and overlap choices. \n \
jump_on_target = int(seg * (1 - overlap)) \n \
jump_on_frame = int(jump_on_frame/shift) "
)
target_len = int(len(frame) * shift)
if smoothing_method == 'mean':
preds = np.zeros(target_len)
pred_count = np.zeros(target_len)
for i, og_pred in enumerate(frame):
if i % jump_on_frame != 0:
continue
start = i * shift
end = start + seg
preds[start:end] = preds[start:end] + og_pred
pred_count[start:end] = pred_count[start:end] + 1
preds = preds / pred_count
last_non_zero_pred = preds[pred_count != 0][-1]
preds[pred_count == 0] = last_non_zero_pred
elif smoothing_method == 'median':
preds = [[] for _ in range(target_len)]
for i, og_pred in enumerate(frame):
if i % jump_on_frame != 0:
continue
start = i * shift
end = start + seg
for j in range(start, end):
if j <= target_len - 1:
preds[j].append(og_pred)
preds = np.array([np.median(l) for l in preds])
nan_idx = np.isnan(preds)
last_non_nan_pred = preds[~nan_idx][-1]
preds[nan_idx] = last_non_nan_pred
else:
raise ValueError("smoothing_method should be either mean or median")
round_final = np.round(preds, 4)
np.savetxt(overlap_filepath, round_final, delimiter='\n')
except Exception as e:
raise (e)
def generate_vad_segment_table(vad_pred_dir, threshold, shift_len, num_workers):
"""
Convert frame level prediction to speech segment in start and end times format.
And save to csv file in rttm-like format
0, 10, speech
17,18, speech
Args:
vad_pred_dir (str): directory of prediction files to be processed.
threshold (float): threshold for prediction score (from 0 to 1).
shift_len (float): amount of shift of window for generating the frame.
out_dir (str): output dir of generated table/csv file.
num_workers(float): number of process for multiprocessing
Returns:
table_out_dir(str): directory of generate table.
"""
p = Pool(processes=num_workers)
suffixes = ("frame", "mean", "median")
vad_pred_filepath_list = [os.path.join(vad_pred_dir, x) for x in os.listdir(vad_pred_dir) if x.endswith(suffixes)]
table_out_dir = os.path.join(vad_pred_dir, "table_output_" + str(threshold))
if not os.path.exists(table_out_dir):
os.mkdir(table_out_dir)
per_args = {
"threshold": threshold,
"shift_len": shift_len,
"out_dir": table_out_dir,
}
p.starmap(generate_vad_segment_table_per_file, zip(vad_pred_filepath_list, repeat(per_args)))
p.close()
p.join()
return table_out_dir
def generate_vad_segment_table_per_file(pred_filepath, per_args):
"""
See discription in generate_overlap_vad_seq.
"""
threshold = per_args['threshold']
shift_len = per_args['shift_len']
out_dir = per_args['out_dir']
name = pred_filepath.split("/")[-1].rsplit(".", 1)[0]
sequence = np.loadtxt(pred_filepath)
start = 0
start_list = [0]
dur_list = []
state_list = []
current_state = "non-speech"
for i in range(len(sequence) - 1):
current_state = "non-speech" if sequence[i] <= threshold else "speech"
next_state = "non-speech" if sequence[i + 1] <= threshold else "speech"
if next_state != current_state:
dur = i * shift_len + shift_len - start # shift_len for handling joint
state_list.append(current_state)
dur_list.append(dur)
start = (i + 1) * shift_len
start_list.append(start)
dur_list.append((i + 1) * shift_len + shift_len - start)
state_list.append(current_state)
seg_table = pd.DataFrame({'start': start_list, 'dur': dur_list, 'vad': state_list})
seg_speech_table = seg_table[seg_table['vad'] == 'speech']
save_name = name + ".txt"
save_path = os.path.join(out_dir, save_name)
seg_speech_table.to_csv(save_path, sep='\t', index=False, header=False)
return save_path
def vad_construct_pyannote_object_per_file(vad_table_filepath, groundtruth_RTTM_file):
"""
Construct pyannote object for evaluation.
Args:
vad_table_filepath(str) : path of vad rttm-like table.
groundtruth_RTTM_file(str): path of groundtruth rttm file.
Returns:
reference(pyannote.Annotation): groundtruth
hypothesise(pyannote.Annotation): prediction
"""
pred = pd.read_csv(vad_table_filepath, sep="\t", header=None)
label = pd.read_csv(groundtruth_RTTM_file, sep=" ", delimiter=None, header=None)
label = label.rename(columns={3: "start", 4: "dur", 7: "speaker"})
# construct reference
reference = Annotation()
for index, row in label.iterrows():
reference[Segment(row['start'], row['start'] + row['dur'])] = row['speaker']
# construct hypothsis
hypothesis = Annotation()
for index, row in pred.iterrows():
hypothesis[Segment(row[0], row[0] + row[1])] = 'Speech'
return reference, hypothesis
def vad_tune_threshold_on_dev(thresholds, vad_pred, groundtruth_RTTM, vad_pred_method="frame", focus_metric="DetER"):
"""
Tune threshold on dev set. Return best threshold which gives the lowest detection error rate (DetER) in thresholds.
Args:
thresholds (list): list of thresholds.
vad_pred_method (str): suffix of prediction file. Use to locate file. Should be either in "frame", "mean" or "median".
vad_pred_dir (str): directory of vad predictions or a file contains the paths of them
groundtruth_RTTM_dir (str): directory of groundtruch rttm files or a file contains the paths of them.
focus_metric (str): metrics we care most when tuning threshold. Should be either in "DetER", "FA", "MISS"
Returns:
best_threhsold (float): threshold that gives lowest DetER.
"""
threshold_perf = {}
best_threhsold = thresholds[0]
min_score = 100
try:
thresholds[0] >= 0 and thresholds[-1] <= 1
except:
raise ValueError("Invalid threshold! Should be in [0, 1]")
for threshold in thresholds:
metric = detection.DetectionErrorRate()
paired_filenames, groundtruth_RTTM_dict, vad_pred_dict = pred_rttm_map(
vad_pred, groundtruth_RTTM, vad_pred_method
)
print(paired_filenames)
for filename in paired_filenames:
vad_pred_filepath = vad_pred_dict[filename]
groundtruth_RTTM_file = groundtruth_RTTM_dict[filename]
if os.path.isdir(vad_pred):
table_out_dir = os.path.join(vad_pred, "table_output_" + str(threshold))
else:
table_out_dir = os.path.join("tmp_table_outputs", "table_output_" + str(threshold))
if not os.path.exists(table_out_dir):
os.makedirs(table_out_dir)
per_args = {"threshold": threshold, "shift_len": 0.01, "out_dir": table_out_dir}
vad_table_filepath = generate_vad_segment_table_per_file(vad_pred_filepath, per_args)
reference, hypothesis = vad_construct_pyannote_object_per_file(vad_table_filepath, groundtruth_RTTM_file)
metric(reference, hypothesis) # accumulation
report = metric.report(display=False)
DetER = report.iloc[[-1]][('detection error rate', '%')].item()
FA = report.iloc[[-1]][('false alarm', '%')].item()
MISS = report.iloc[[-1]][('miss', '%')].item()
if focus_metric == "DetER":
score = DetER
elif focus_metric == "FA":
score = FA
elif focus_metric == "MISS":
score = MISS
else:
raise ValueError("Metric we care most should be only in 'DetER', 'FA'or 'MISS'!")
threshold_perf[threshold] = {'DetER (%)': DetER, 'FA (%)': FA, 'MISS (%)': MISS}
logging.info(f"threshold {threshold}, {threshold_perf[threshold]}")
del report
metric.reset() # reset internal accumulator
if score < min_score:
min_score = score
best_threhsold = threshold
return best_threhsold
def pred_rttm_map(vad_pred, groundtruth_RTTM, vad_pred_method="frame"):
"""
Find paired files in vad_pred and groundtruth_RTTM
"""
groundtruth_RTTM_dict = {}
if os.path.isfile(groundtruth_RTTM):
with open(groundtruth_RTTM, "r") as fp:
groundtruth_RTTM_files = fp.read().splitlines()
elif os.path.isdir(groundtruth_RTTM):
groundtruth_RTTM_files = glob.glob(os.path.join(groundtruth_RTTM, "*.rttm"))
else:
raise ValueError(
"groundtruth_RTTM should either be a directory contains rttm files or a file contains paths to them!"
)
for f in groundtruth_RTTM_files:
filename = os.path.basename(f).rsplit(".", 1)[0]
groundtruth_RTTM_dict[filename] = f
vad_pred_dict = {}
if os.path.isfile(vad_pred):
with open(vad_pred, "r") as fp:
vad_pred_files = fp.read().splitlines()
elif os.path.isdir(vad_pred):
vad_pred_files = glob.glob(os.path.join(vad_pred, "*." + vad_pred_method))
else:
raise ValueError(
"vad_pred should either be a directory contains vad pred files or a file contains paths to them!"
)
for f in vad_pred_files:
filename = os.path.basename(f).rsplit(".", 1)[0]
vad_pred_dict[filename] = f
paired_filenames = groundtruth_RTTM_dict.keys() & vad_pred_dict.keys()
return paired_filenames, groundtruth_RTTM_dict, vad_pred_dict
def plot(path2audio_file, path2_vad_pred, path2ground_truth_label=None, threshold=0.85):
"""
Plot VAD outputs for demonstration in tutorial
Args:
path2audio_file (str): path to audio file.
path2_vad_pred (str): path to vad prediction file,
path2ground_truth_label(str): path to groundtruth label file.
threshold (float): threshold for prediction score (from 0 to 1).
"""
plt.figure(figsize=[20, 2])
FRAME_LEN = 0.01
audio, sample_rate = librosa.load(path=path2audio_file, sr=16000, mono=True)
dur = librosa.get_duration(audio, sr=sample_rate)
time = np.arange(0, dur, FRAME_LEN)
frame = np.loadtxt(path2_vad_pred)
len_pred = len(frame)
ax1 = plt.subplot()
ax1.plot(np.arange(audio.size) / sample_rate, audio, 'gray')
ax1.set_xlim([0, int(dur) + 1])
ax1.tick_params(axis='y', labelcolor='b')
ax1.set_ylabel('Signal')
ax1.set_ylim([-1, 1])
ax2 = ax1.twinx()
prob = frame
pred = np.where(prob >= threshold, 1, 0)
if path2ground_truth_label:
label = extract_labels(path2ground_truth_label, time)
ax2.plot(np.arange(len_pred) * FRAME_LEN, label, 'r', label='label')
ax2.plot(np.arange(len_pred) * FRAME_LEN, pred, 'b', label='pred')
ax2.plot(np.arange(len_pred) * FRAME_LEN, prob, 'g--', label='speech prob')
ax2.tick_params(axis='y', labelcolor='r')
ax2.legend(loc='lower right', shadow=True)
ax2.set_ylabel('Preds and Probas')
ax2.set_ylim([-0.1, 1.1])
return None
def extract_labels(path2ground_truth_label, time):
"""
Extract groundtruth label for given time period.
path2ground_truth_label (str): path of groundtruth label file
time (list) : a list of array represent time period.
"""
data = pd.read_csv(path2ground_truth_label, sep=" ", delimiter=None, header=None)
data = data.rename(columns={3: "start", 4: "dur", 7: "speaker"})
labels = []
for pos in time:
line = data[(data["start"] <= pos) & (data["start"] + data["dur"] > pos)]
if len(line) >= 1:
labels.append(1)
else:
labels.append(0)
return labels
+26
View File
@@ -0,0 +1,26 @@
# Copyright (c) 2020, 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 nemo.collections.common.callbacks
from nemo.collections.common import data, losses, parts, tokenizers
from nemo.package_info import __version__
# Set collection version equal to NeMo version.
__version = __version__
# Authorship.
__author__ = "NVIDIA Corporation"
# Set collection name.
__description__ = "Common collection"
@@ -0,0 +1,15 @@
# Copyright (c) 2020, 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.
from nemo.collections.common.callbacks.callbacks import LogEpochTimeCallback
@@ -0,0 +1,97 @@
# Copyright (c) 2020, 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 time
import numpy as np
from pytorch_lightning.callbacks.base import Callback
from pytorch_lightning.utilities import rank_zero_only
# from sacrebleu import corpus_bleu
class LogEpochTimeCallback(Callback):
"""Simple callback that logs how long each epoch takes, in seconds, to a pytorch lightning log
"""
@rank_zero_only
def on_train_epoch_start(self, trainer, pl_module):
self.epoch_start = time.time()
@rank_zero_only
def on_train_epoch_end(self, trainer, pl_module, outputs):
curr_time = time.time()
duration = curr_time - self.epoch_start
trainer.logger.log_metrics({"epoch_time": duration}, step=trainer.global_step)
# class MachineTranslationLogEvalCallback(Callback):
# def _on_eval_end(self, trainer, pl_module, mode):
# counts = np.array(self._non_pad_tokens)
# eval_loss = np.sum(np.array(self._losses) * counts) / np.sum(counts)
# sacre_bleu = corpus_bleu(self._translations, [self._ground_truths], tokenize="13a")
# print(f"{mode} results for process with global rank {pl_module.global_rank}".upper())
# for i in range(pl_module.num_examples[mode]):
# print('\u0332'.join(f"EXAMPLE {i}:")) # Underline output
# sent_id = np.random.randint(len(self._translations))
# print(f"Ground truth: {self._ground_truths[sent_id]}\n")
# print(f"Translation: {self._translations[sent_id]}\n")
# print()
# print("-" * 50)
# print(f"loss: {eval_loss:.3f}")
# print(f"SacreBLEU: {sacre_bleu}")
# print("-" * 50)
# @rank_zero_only
# def on_test_end(self, trainer, pl_module):
# self._on_eval_end(trainer, pl_module, "test")
# @rank_zero_only
# def on_validation_end(self, trainer, pl_module):
# self._on_eval_end(trainer, pl_module, "val")
# @rank_zero_only
# def on_sanity_check_end(self, trainer, pl_module):
# self._on_eval_end(trainer, pl_module, "val")
# def _on_eval_batch_end(self, trainer, pl_module, outputs, batch, batch_idx, dataloader_idx, mode):
# self._translations.extend(outputs['translations'])
# self._ground_truths.extend(outputs['ground_truths'])
# self._non_pad_tokens.append(outputs['num_non_pad_tokens'])
# self._losses.append(outputs[f'{mode}_loss'])
# @rank_zero_only
# def on_test_batch_end(self, trainer, pl_module, batch, outputs, batch_idx, dataloader_idx):
# self._on_eval_batch_end(trainer, pl_module, batch, outputs, batch_idx, dataloader_idx, 'test')
# @rank_zero_only
# def on_validation_batch_end(self, trainer, pl_module, batch, outputs, batch_idx, dataloader_idx):
# self._on_eval_batch_end(trainer, pl_module, batch, outputs, batch_idx, dataloader_idx, 'val')
# def _on_eval_start(self, trainer, pl_module):
# self._translations = []
# self._ground_truths = []
# self._losses = []
# self._non_pad_tokens = []
# @rank_zero_only
# def on_test_start(self, trainer, pl_module):
# self._on_eval_start(trainer, pl_module)
# @rank_zero_only
# def on_validation_start(self, trainer, pl_module):
# self._on_eval_start(trainer, pl_module)
# @rank_zero_only
# def on_sanity_check_start(self, trainer, pl_module):
# self._on_eval_start(trainer, pl_module)
@@ -0,0 +1,15 @@
# Copyright (c) 2020, 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.
from nemo.collections.common.data.dataset import ConcatDataset
+178
View File
@@ -0,0 +1,178 @@
# Copyright (c) 2020, 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.
from typing import Any, List
import numpy as np
import torch.utils.data as pt_data
from torch.utils.data import IterableDataset
__all__ = ['ConcatDataset']
class ConcatDataset(IterableDataset):
"""
A dataset that accepts as argument multiple datasets and then samples from them based on the specified
sampling technique.
Args:
datasets (list): A list of datasets to sample from.
shuffle (bool): Whether to shuffle individual datasets. Only works with non-iterable datasets.
Defaults to True.
sampling_technique (str): Sampling technique to choose which dataset to draw a sample from.
Defaults to 'temperature'. Currently supports 'temperature', 'random' and 'round-robin'.
sampling_temperature (int): Temperature value for sampling. Only used when sampling_technique = 'temperature'.
Defaults to 5.
sampling_probabilities (list): Probability values for sampling. Only used when sampling_technique = 'random'.
global_rank (int): Worker rank, used for partitioning map style datasets. Defaults to 0.
world_size (int): Total number of processes, used for partitioning map style datasets. Defaults to 1.
"""
def __init__(
self,
datasets: List[Any],
shuffle: bool = True,
sampling_technique: str = 'temperature',
sampling_temperature: int = 5,
sampling_probabilities: List[float] = None,
global_rank: int = 0,
world_size: int = 1,
):
super().__init__()
supported_sampling_techniques = ['temperature', 'random', 'round-robin']
self.datasets = datasets
self.iterables = [None] * len(datasets)
self.shuffle = shuffle
self.global_rank = global_rank
self.world_size = world_size
self.sampling_kwargs = {}
if sampling_technique == 'temperature':
self.index_generator = ConcatDataset.temperature_generator
self.sampling_kwargs['temperature'] = sampling_temperature
elif sampling_technique == 'random':
self.index_generator = ConcatDataset.random_generator
self.sampling_kwargs['p'] = sampling_probabilities
elif sampling_technique == 'round-robin':
self.index_generator = ConcatDataset.round_robin_generator
else:
raise ValueError(f"Currently we only support sampling techniques in {supported_sampling_techniques}.")
self.length = 0
if isinstance(datasets[0], IterableDataset):
self.kind = 'iterable'
else:
self.kind = 'map'
for idx, dataset in enumerate(datasets):
isiterable = isinstance(dataset, IterableDataset)
if (isiterable and not self.kind == 'iterable') or (not isiterable and self.kind == 'iterable'):
raise ValueError("All datasets in ConcatDataset must be of the same kind (Iterable or Map).")
if self.kind == 'map':
self.length += len(dataset) // world_size
else:
self.length += len(dataset)
def get_iterable(self, dataset):
if isinstance(dataset, IterableDataset):
return dataset.__iter__()
else:
indices = np.arange(len(dataset))
if self.shuffle:
np.random.shuffle(indices)
return iter(indices)
def __iter__(self):
worker_info = pt_data.get_worker_info()
if worker_info is None:
max_elements = self.length
wid = 0
wnum = 1
else:
wid = worker_info.id
wnum = worker_info.num_workers
max_elements = len(range(wid, self.length, wnum))
if self.kind == 'map':
for idx in range(len(self.datasets)):
start_idx = (len(self.datasets[idx]) // self.world_size) * self.global_rank
end_idx = start_idx + (len(self.datasets[idx]) // self.world_size)
if self.global_rank == self.world_size - 1:
end_idx = len(self.datasets[idx])
indices = range(start_idx + wid, end_idx, wnum)
self.datasets[idx] = pt_data.Subset(self.datasets[idx], indices)
for idx, dataset in enumerate(self.datasets):
iterable = self.get_iterable(dataset)
self.iterables[idx] = iterable
n = 0
ind_gen = self.index_generator(self.datasets, **self.sampling_kwargs)
while n < max_elements:
n += 1
try:
ind = next(ind_gen)
except StopIteration:
return
try:
val = next(self.iterables[ind])
if self.kind == 'map':
val = self.datasets[ind][val]
yield val
except StopIteration:
self.iterables[ind] = self.get_iterable(self.datasets[ind])
n -= 1
def __len__(self):
return self.length
@staticmethod
def temperature_generator(datasets, **kwargs):
temp = kwargs.get('temperature')
if not temp:
raise ValueError("Temperature generator expects a 'temperature' keyowrd argument.")
lengths = []
num = len(datasets)
for dataset in datasets:
lengths.append(len(dataset))
p = np.array(lengths) / np.sum(lengths)
p = np.power(p, 1 / temp)
p = p / np.sum(p)
while True:
ind = np.random.choice(np.arange(num), p=p)
yield ind
@staticmethod
def round_robin_generator(datasets, **kwargs):
num = len(datasets)
while True:
for i in range(num):
yield i
@staticmethod
def random_generator(datasets, **kwargs):
p = kwargs.get('p')
if not p:
raise ValueError("Random generator expects a 'p' keyowrd argument for sampling probabilities.")
num = len(datasets)
if len(p) != num:
raise ValueError("Length of probabilities list must be equal to the number of datasets.")
while True:
ind = np.random.choice(np.arange(num), p=p)
yield ind
@@ -0,0 +1,20 @@
# Copyright (c) 2020, 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.
from nemo.collections.common.losses.aggregator import AggregatorLoss
from nemo.collections.common.losses.cross_entropy import CrossEntropyLoss, NLLLoss
from nemo.collections.common.losses.mse_loss import MSELoss
from nemo.collections.common.losses.multi_similarity_loss import MultiSimilarityLoss
from nemo.collections.common.losses.smoothed_cross_entropy import SmoothedCrossEntropyLoss
from nemo.collections.common.losses.spanning_loss import SpanningLoss
@@ -0,0 +1,67 @@
# Copyright (c) 2020, 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.
from typing import List
import torch
from nemo.core.classes import Loss, typecheck
from nemo.core.neural_types import LossType, NeuralType
__all__ = ['AggregatorLoss']
class AggregatorLoss(Loss):
"""
Sums several losses into one.
Args:
num_inputs: number of input losses
weights: a list of coefficient for merging losses
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
input_types = {}
for i in range(self._num_losses):
input_types["loss_" + str(i + 1)] = NeuralType(elements_type=LossType())
return input_types
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, num_inputs: int = 2, weights: List[float] = None):
super().__init__()
self._num_losses = num_inputs
if weights is not None and len(weights) != num_inputs:
raise ValueError("Length of weights should be equal to the number of inputs (num_inputs)")
self._weights = weights
@typecheck()
def forward(self, **kwargs):
values = [kwargs[x] for x in sorted(kwargs.keys())]
loss = torch.zeros_like(values[0])
for loss_idx, loss_value in enumerate(values):
if self._weights is not None:
loss = loss.add(loss_value, alpha=self._weights[loss_idx])
else:
loss = loss.add(loss_value)
return loss
@@ -0,0 +1,138 @@
# Copyright (c) 2020, 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 torch
from torch import nn
from nemo.core.classes import Serialization, Typing, typecheck
from nemo.core.neural_types import LabelsType, LogitsType, LogprobsType, LossType, MaskType, NeuralType
__all__ = ['CrossEntropyLoss', 'NLLLoss']
class CrossEntropyLoss(nn.CrossEntropyLoss, Serialization, Typing):
"""
CrossEntropyLoss
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"logits": NeuralType(['B'] + ['ANY'] * (self._logits_dim - 1), LogitsType()),
"labels": NeuralType(['B'] + ['ANY'] * (self._logits_dim - 2), LabelsType()),
"loss_mask": NeuralType(['B'] + ['ANY'] * (self._logits_dim - 2), MaskType(), optional=True),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, logits_ndim=2, weight=None, reduction='mean', ignore_index=-100):
"""
Args:
logits_ndim (int): number of dimensions (or rank) of the logits tensor
weight (list): list of rescaling weight given to each class
reduction (str): type of the reduction over the batch
"""
if weight is not None and not torch.is_tensor(weight):
weight = torch.FloatTensor(weight)
super().__init__(weight=weight, reduction=reduction, ignore_index=ignore_index)
self._logits_dim = logits_ndim
@typecheck()
def forward(self, logits, labels, loss_mask=None):
"""
Args:
logits (float): output of the classifier
labels (long): ground truth labels
loss_mask (bool/float/int): tensor to specify the masking
"""
logits_flatten = torch.flatten(logits, start_dim=0, end_dim=-2)
labels_flatten = torch.flatten(labels, start_dim=0, end_dim=-1)
if loss_mask is not None:
if loss_mask.dtype is not torch.bool:
loss_mask = loss_mask > 0.5
loss_mask_flatten = torch.flatten(loss_mask, start_dim=0, end_dim=-1)
logits_flatten = logits_flatten[loss_mask_flatten]
labels_flatten = labels_flatten[loss_mask_flatten]
if len(labels_flatten) == 0:
return super().forward(logits, torch.argmax(logits, dim=-1))
loss = super().forward(logits_flatten, labels_flatten)
return loss
class NLLLoss(nn.NLLLoss, Serialization, Typing):
"""
NLLLoss
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"log_probs": NeuralType(("B", "T", "D"), LogprobsType()),
"labels": NeuralType(("B", "T"), LabelsType()),
"output_mask": NeuralType(("B", "T"), MaskType(), optional=True),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, log_probs_ndim=2, weight=None, reduction='mean', ignore_index=-100):
"""
Args:
log_probs_ndim (int): number of dimensions (or rank) of the logprobs tensor
weight (list): list of rescaling weight given to each class
reduction (str): type of the reduction over the batch
ignore_index (int): mask out loss computation where labels = ignore_index
"""
if weight is not None and not torch.is_tensor(weight):
weight = torch.FloatTensor(weight)
super().__init__(weight=weight, reduction=reduction, ignore_index=ignore_index)
self._log_probs_dim = log_probs_ndim
@typecheck()
def forward(self, log_probs, labels, loss_mask=None):
"""
Args:
log_probs (float): output log probability tensor
labels (long): ground truth labels
loss_mask (bool/float/int): tensor to specify the masking
"""
log_probs_flatten = torch.flatten(log_probs, start_dim=0, end_dim=-2)
labels_flatten = torch.flatten(labels, start_dim=0, end_dim=-1)
if loss_mask is not None:
if loss_mask.dtype is not torch.bool:
loss_mask = loss_mask > 0.5
loss_mask_flatten = torch.flatten(loss_mask, start_dim=0, end_dim=-1)
log_probs_flatten = log_probs_flatten[loss_mask_flatten]
labels_flatten = labels_flatten[loss_mask_flatten]
if len(labels_flatten) == 0:
return super().forward(log_probs, torch.argmax(log_probs, dim=-1))
loss = super().forward(log_probs_flatten, labels_flatten)
return loss
@@ -0,0 +1,57 @@
# Copyright (c) 2020, 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.
from torch import Tensor, nn
from nemo.core.classes import Serialization, Typing, typecheck
from nemo.core.neural_types import LabelsType, LossType, NeuralType, RegressionValuesType
__all__ = ['MSELoss']
class MSELoss(nn.MSELoss, Serialization, Typing):
"""
MSELoss
"""
@property
def input_types(self):
"""Returns definitions of module input ports.
"""
return {
"preds": NeuralType(tuple('B'), RegressionValuesType()),
"labels": NeuralType(tuple('B'), LabelsType()),
}
@property
def output_types(self):
"""Returns definitions of module output ports.
"""
return {"loss": NeuralType(elements_type=LossType())}
def __init__(self, reduction: str = 'mean'):
"""
Args:
reduction: type of the reduction over the batch
"""
super().__init__(reduction=reduction)
@typecheck()
def forward(self, preds: Tensor, labels: Tensor) -> Tensor:
"""
Args:
preds: output of the classifier
labels: ground truth labels
"""
return super().forward(preds, labels)

Some files were not shown because too many files have changed in this diff Show More