ba4be087d5
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
270 lines
10 KiB
Python
270 lines
10 KiB
Python
# Copyright (c) 2023, 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
|
|
|
|
import lightning.pytorch as pl
|
|
import pytest
|
|
import torch
|
|
from omegaconf import DictConfig, ListConfig
|
|
|
|
from nemo.collections.asr.models import EncDecCTCModel, EncDecHybridRNNTCTCModel
|
|
from nemo.collections.asr.parts.submodules.ctc_decoding import CTCDecodingConfig
|
|
from nemo.core.classes.mixins import AccessMixin
|
|
|
|
|
|
def jasper_encoder_config(num_layers=1) -> Dict:
|
|
return {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASREncoder',
|
|
'feat_in': 64,
|
|
'activation': 'relu',
|
|
'conv_mask': True,
|
|
'jasper': [
|
|
{
|
|
'filters': 4,
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
}
|
|
]
|
|
* num_layers,
|
|
}
|
|
|
|
|
|
def conformer_encoder_config() -> Dict:
|
|
return {
|
|
'_target_': 'nemo.collections.asr.modules.ConformerEncoder',
|
|
'feat_in': 64,
|
|
'n_layers': 8,
|
|
'd_model': 4,
|
|
}
|
|
|
|
|
|
class TestInterCTCLoss:
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize(
|
|
"model_class",
|
|
[EncDecCTCModel, EncDecHybridRNNTCTCModel],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"encoder_config",
|
|
[jasper_encoder_config(num_layers=8), conformer_encoder_config()],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"apply_at_layers,loss_weights",
|
|
[
|
|
([2, 4], [0.1, 0.3]),
|
|
([4], [0.3]),
|
|
([], []),
|
|
# errors
|
|
([2, 4], [0.1]),
|
|
([2], [0.1, 0.3]),
|
|
([], [0.3]),
|
|
],
|
|
)
|
|
def test_forward(self, model_class, encoder_config, apply_at_layers, loss_weights):
|
|
preprocessor_config = {'_target_': 'nemo.collections.asr.modules.AudioToMelSpectrogramPreprocessor'}
|
|
vocabulary = [
|
|
' ',
|
|
'a',
|
|
'b',
|
|
'c',
|
|
'd',
|
|
'e',
|
|
'f',
|
|
'g',
|
|
'h',
|
|
'i',
|
|
'j',
|
|
'k',
|
|
'l',
|
|
'm',
|
|
'n',
|
|
'o',
|
|
'p',
|
|
'q',
|
|
'r',
|
|
's',
|
|
't',
|
|
'u',
|
|
'v',
|
|
'w',
|
|
'x',
|
|
'y',
|
|
'z',
|
|
"'",
|
|
]
|
|
if model_class is EncDecCTCModel:
|
|
decoder_config = {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASRDecoder',
|
|
'feat_in': None,
|
|
'num_classes': len(vocabulary),
|
|
'vocabulary': vocabulary,
|
|
}
|
|
model_config = DictConfig(
|
|
{
|
|
'compute_eval_loss': True, # will be ignored by the model
|
|
'preprocessor': DictConfig(preprocessor_config),
|
|
'encoder': DictConfig(encoder_config),
|
|
'decoder': DictConfig(decoder_config),
|
|
}
|
|
)
|
|
else:
|
|
decoder_config = {
|
|
'_target_': 'nemo.collections.asr.modules.RNNTDecoder',
|
|
'prednet': {'pred_hidden': 4, 'pred_rnn_layers': 1},
|
|
}
|
|
joint_config = {
|
|
'_target_': 'nemo.collections.asr.modules.RNNTJoint',
|
|
'jointnet': {'joint_hidden': 4, 'activation': 'relu'},
|
|
}
|
|
decoding_config = {'strategy': 'greedy_batch', 'greedy': {'max_symbols': 30}}
|
|
loss_config = {'loss_name': 'default', 'warprnnt_numba_kwargs': {'fastemit_lambda': 0.001}}
|
|
|
|
aux_ctc_config = {
|
|
'ctc_loss_weight': 0.3,
|
|
'use_cer': False,
|
|
'ctc_reduction': 'mean_batch',
|
|
'decoder': {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASRDecoder',
|
|
'feat_in': None,
|
|
'num_classes': len(vocabulary),
|
|
'vocabulary': vocabulary,
|
|
},
|
|
'decoding': DictConfig(CTCDecodingConfig),
|
|
}
|
|
model_config = DictConfig(
|
|
{
|
|
'compute_eval_loss': True,
|
|
'labels': ListConfig(vocabulary),
|
|
'preprocessor': DictConfig(preprocessor_config),
|
|
'model_defaults': DictConfig({'enc_hidden': 4, 'pred_hidden': 4}),
|
|
'encoder': DictConfig(encoder_config),
|
|
'decoder': DictConfig(decoder_config),
|
|
'joint': DictConfig(joint_config),
|
|
'decoding': DictConfig(decoding_config),
|
|
'loss': DictConfig(loss_config),
|
|
'aux_ctc': DictConfig(aux_ctc_config),
|
|
}
|
|
)
|
|
model_config.update(
|
|
{
|
|
'interctc': {'loss_weights': loss_weights, 'apply_at_layers': apply_at_layers},
|
|
'optim': {'name': 'adamw'},
|
|
}
|
|
)
|
|
|
|
class DummyDataset(torch.utils.data.Dataset):
|
|
"""Simply returns a single set of values."""
|
|
|
|
def __init__(self, values):
|
|
self.values = values
|
|
|
|
def __len__(self):
|
|
return 1
|
|
|
|
def __getitem__(self, idx):
|
|
return self.values
|
|
|
|
# this sometimes results in all zeros in the output which breaks tests
|
|
# so using this only for the ptl calls in the bottom, but using
|
|
# processed signal directly initially to remove the chance of
|
|
# this edge-case
|
|
input_signal = torch.randn(size=(1, 512))
|
|
input_length = torch.randint(low=321, high=500, size=[1])
|
|
target = torch.randint(size=(1, input_length[0]), low=0, high=28)
|
|
target_length = torch.tensor([input_length[0]])
|
|
|
|
processed_signal = torch.randn(size=([1, 64, 12]))
|
|
processed_length = torch.tensor([8])
|
|
|
|
if len(apply_at_layers) != len(loss_weights):
|
|
# has to throw an error here
|
|
with pytest.raises(
|
|
ValueError, match="Length of interctc.apply_at_layers has to match interctc.loss_weights"
|
|
):
|
|
asr_model = model_class(cfg=model_config)
|
|
asr_model.train()
|
|
logprobs, _, _ = asr_model.forward(input_signal=input_signal, input_signal_length=input_length)
|
|
else:
|
|
asr_model = model_class(cfg=model_config)
|
|
asr_model.train()
|
|
AccessMixin.set_access_enabled(access_enabled=True, guid=asr_model.model_guid)
|
|
logprobs, *_ = asr_model.forward(
|
|
processed_signal=processed_signal, processed_signal_length=processed_length
|
|
)
|
|
captured_tensors = asr_model.get_captured_interctc_tensors()
|
|
AccessMixin.reset_registry(asr_model)
|
|
assert len(captured_tensors) == len(apply_at_layers)
|
|
for output in captured_tensors:
|
|
# checking that values are not the same, if shape is the same
|
|
assert output[0].shape != logprobs.shape or not torch.allclose(output[0], logprobs)
|
|
# hybrid model returns output of encoder, so it's not expected to match
|
|
if model_class is EncDecCTCModel:
|
|
assert output[0].shape == logprobs.shape
|
|
|
|
# Explicitly pass accelerator as cpu, since default val in PTL >= 2.0 is auto and it picks cuda
|
|
# which further causes an error in all reduce at: https://github.com/NVIDIA/NeMo/blob/v1.18.1/nemo/collections/asr/modules/conv_asr.py#L209
|
|
trainer = pl.Trainer(max_epochs=1, accelerator='cpu')
|
|
trainer.fit(
|
|
asr_model,
|
|
train_dataloaders=torch.utils.data.DataLoader(
|
|
DummyDataset([input_signal, input_length, target, target_length]),
|
|
collate_fn=lambda x: x[0],
|
|
),
|
|
val_dataloaders=torch.utils.data.DataLoader(
|
|
DummyDataset([input_signal, input_length, target, target_length]),
|
|
collate_fn=lambda x: x[0],
|
|
),
|
|
)
|
|
required_metrics = ['final_loss'] if len(loss_weights) > 0 else []
|
|
required_metrics += [f'inter_ctc_loss_l{idx}' for idx in apply_at_layers]
|
|
prefix = "val_"
|
|
required_metrics += [f'{prefix}{metric}' for metric in required_metrics]
|
|
required_metrics += [f'{prefix}wer'] + [f'{prefix}inter_wer_l{idx}' for idx in apply_at_layers]
|
|
for metric in required_metrics:
|
|
if 'loss' in metric and 'val_' in metric:
|
|
if model_config['compute_eval_loss']:
|
|
assert metric in trainer.logged_metrics
|
|
else:
|
|
assert metric not in trainer.logged_metrics
|
|
else:
|
|
assert metric in trainer.logged_metrics
|
|
|
|
trainer.test(
|
|
asr_model,
|
|
dataloaders=torch.utils.data.DataLoader(
|
|
DummyDataset([input_signal, input_length, target, target_length]),
|
|
collate_fn=lambda x: x[0],
|
|
),
|
|
)
|
|
required_metrics = [f'inter_ctc_loss_l{idx}' for idx in apply_at_layers]
|
|
prefix = 'test_'
|
|
# note that "=" is on purpose here, not "+=", since we only log test metrics
|
|
required_metrics = [f'{prefix}{metric}' for metric in required_metrics]
|
|
required_metrics += [f'{prefix}wer'] + [f'{prefix}inter_wer_l{idx}' for idx in apply_at_layers]
|
|
for metric in required_metrics:
|
|
if 'loss' in metric:
|
|
if model_config['compute_eval_loss']:
|
|
assert metric in trainer.logged_metrics
|
|
else:
|
|
assert metric not in trainer.logged_metrics
|
|
else:
|
|
assert metric in trainer.logged_metrics
|