ba4be087d5
CICD NeMo / cicd-main-unit-tests (push) Blocked by required conditions
CICD NeMo / cicd-main-speech (push) Blocked by required conditions
CICD NeMo / cicd-test-container-build (push) Blocked by required conditions
CICD NeMo / cicd-import-tests (push) Blocked by required conditions
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Blocked by required conditions
CICD NeMo / Nemo_CICD_Test (push) Blocked by required conditions
CICD NeMo / Coverage (e2e) (push) Blocked by required conditions
CICD NeMo / Coverage (unit-test) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
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
CICD NeMo / cicd-wait-in-queue (push) Waiting to run
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
447 lines
16 KiB
Python
447 lines
16 KiB
Python
# Copyright (c) 2022, 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 pytest
|
|
import torch
|
|
from omegaconf import DictConfig
|
|
|
|
from nemo.collections.asr.losses import ContrastiveLoss
|
|
from nemo.collections.asr.models import EncDecDenoiseMaskedTokenPredModel, SpeechEncDecSelfSupervisedModel
|
|
from nemo.core.classes.common import typecheck
|
|
|
|
|
|
@pytest.fixture()
|
|
def ssl_model():
|
|
preprocessor = {
|
|
'cls': 'nemo.collections.asr.modules.AudioToMelSpectrogramPreprocessor',
|
|
'params': dict({'pad_to': 16, 'dither': 0}),
|
|
}
|
|
|
|
model_defaults = {'enc_hidden': 32, 'dec_out': 128}
|
|
|
|
encoder = {
|
|
'cls': 'nemo.collections.asr.modules.ConvASREncoder',
|
|
'params': {
|
|
'feat_in': 64,
|
|
'activation': 'relu',
|
|
'conv_mask': True,
|
|
'jasper': [
|
|
{
|
|
'filters': model_defaults['enc_hidden'],
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
},
|
|
{
|
|
'filters': model_defaults['enc_hidden'],
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
},
|
|
{
|
|
'filters': model_defaults['enc_hidden'],
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
spec_augment = {
|
|
'_target_': 'nemo.collections.asr.modules.MaskedPatchAugmentation',
|
|
'freq_masks': 3,
|
|
'freq_width': 20,
|
|
'patch_size': 16,
|
|
'mask_patches': 0.5,
|
|
}
|
|
|
|
loss_list_contr_mlm = {
|
|
'contr': {
|
|
'decoder': {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASRDecoderReconstruction',
|
|
'feat_in': model_defaults['enc_hidden'],
|
|
'feat_hidden': 128,
|
|
'feat_out': model_defaults['dec_out'],
|
|
'stride_layers': 0,
|
|
'non_stride_layers': 0,
|
|
'stride_transpose': False,
|
|
},
|
|
'loss': {
|
|
'_target_': 'nemo.collections.asr.losses.ContrastiveLoss',
|
|
'in_dim': 64,
|
|
'proj_dim': model_defaults['dec_out'],
|
|
'combine_time_steps': 1,
|
|
'quantized_targets': True,
|
|
'codebook_size': 64,
|
|
'sample_from_same_utterance_only': True,
|
|
'sample_from_non_masked': False,
|
|
'num_negatives': 3,
|
|
},
|
|
},
|
|
'mlm': {
|
|
'decoder': {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASRDecoder',
|
|
'feat_in': model_defaults['enc_hidden'],
|
|
'num_classes': 4096,
|
|
},
|
|
'loss': {'_target_': 'nemo.collections.asr.losses.MLMLoss', 'combine_time_steps': 1},
|
|
'targets_from_loss': "contr",
|
|
},
|
|
}
|
|
|
|
modelConfig_contr_mlm = DictConfig(
|
|
{
|
|
'preprocessor': DictConfig(preprocessor),
|
|
'spec_augment': DictConfig(spec_augment),
|
|
'model_defaults': DictConfig(model_defaults),
|
|
'encoder': DictConfig(encoder),
|
|
'loss_list': DictConfig(loss_list_contr_mlm),
|
|
}
|
|
)
|
|
ssl_model = SpeechEncDecSelfSupervisedModel(cfg=modelConfig_contr_mlm)
|
|
return ssl_model
|
|
|
|
|
|
@pytest.fixture()
|
|
def denoise_mlm_ssl_model():
|
|
|
|
model_defaults = {
|
|
"subsampling_factor": 1,
|
|
'enc_hidden': 32,
|
|
'dec_out': 128,
|
|
"sample_rate": 16000,
|
|
"num_classes": 32,
|
|
"num_books": 1,
|
|
"code_dim": 16,
|
|
"squeeze_single": False,
|
|
"mask_position": "pre_conv", # position to apply masking, before or after conv subsampling, choices in ['pre_conv', 'post_conv']
|
|
}
|
|
|
|
preprocessor = {
|
|
"_target_": "nemo.collections.asr.modules.AudioToMelSpectrogramPreprocessor",
|
|
"sample_rate": model_defaults["sample_rate"],
|
|
"normalize": "per_feature",
|
|
"window_size": 0.025,
|
|
"window_stride": 0.01,
|
|
"window": "hann",
|
|
"features": 80,
|
|
"n_fft": 512,
|
|
"log": True,
|
|
"frame_splicing": 1,
|
|
"dither": 0.00001,
|
|
"pad_to": 16,
|
|
"pad_value": 0.0,
|
|
}
|
|
|
|
encoder = {
|
|
'cls': 'nemo.collections.asr.modules.ConvASREncoder',
|
|
'params': {
|
|
'feat_in': preprocessor["features"],
|
|
'activation': 'relu',
|
|
'conv_mask': True,
|
|
'jasper': [
|
|
{
|
|
'filters': model_defaults['enc_hidden'],
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
},
|
|
{
|
|
'filters': model_defaults['enc_hidden'],
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
},
|
|
{
|
|
'filters': model_defaults['enc_hidden'],
|
|
'repeat': 1,
|
|
'kernel': [1],
|
|
'stride': [1],
|
|
'dilation': [1],
|
|
'dropout': 0.0,
|
|
'residual': False,
|
|
'separable': True,
|
|
'se': True,
|
|
'se_context_size': -1,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
spec_augment = {
|
|
'_target_': 'nemo.collections.asr.modules.SpectrogramAugmentation',
|
|
'freq_masks': 0,
|
|
'time_masks': 0,
|
|
'freq_width': 16,
|
|
'time_width': 0.05,
|
|
}
|
|
|
|
masking = {
|
|
"_target_": "nemo.collections.asr.modules.RandomBlockMasking",
|
|
"block_size": 40, # for pre_conv masking, 10ms per frame, 400ms per block with block_size=40
|
|
"mask_prob": 0.01, # for allow_overlap=True, this means the mask prob for each frame; otherwise it means the overall masked proportion
|
|
"feat_in": preprocessor["features"],
|
|
"freeze": True,
|
|
"allow_overlap": True,
|
|
}
|
|
|
|
quantizer = {
|
|
"_target_": "nemo.collections.asr.modules.RandomProjectionVectorQuantizer",
|
|
"feat_in": preprocessor["features"],
|
|
"code_dim": model_defaults["code_dim"],
|
|
"num_books": model_defaults["num_books"],
|
|
"num_classes": model_defaults["num_classes"],
|
|
"dist_fn": "l2", # choices=["l2", "cosine"]
|
|
"freeze": True,
|
|
"squeeze_single": model_defaults["squeeze_single"],
|
|
"combine_time_steps": model_defaults["subsampling_factor"], # conformer sub-sampling ratio
|
|
}
|
|
|
|
decoder = {
|
|
"_target_": "nemo.collections.asr.modules.MultiSoftmaxDecoder",
|
|
"feat_in": model_defaults["enc_hidden"],
|
|
"num_classes": model_defaults["num_classes"],
|
|
"num_decoders": model_defaults["num_books"],
|
|
"squeeze_single": model_defaults["squeeze_single"],
|
|
"use_bias": True,
|
|
}
|
|
|
|
loss = {
|
|
"_target_": "nemo.collections.asr.losses.MultiMLMLoss",
|
|
"combine_time_steps": model_defaults[
|
|
"subsampling_factor"
|
|
], # conformer sub-sampling ratio for 'pre_conv', 1 for 'post_conv'
|
|
"mask_threshold": 0.8,
|
|
"num_decoders": model_defaults["num_books"],
|
|
"squeeze_single": model_defaults["squeeze_single"],
|
|
}
|
|
|
|
optim = {
|
|
"name": "adamw",
|
|
"lr": 5.0,
|
|
# optimizer arguments
|
|
"betas": [0.9, 0.98],
|
|
"weight_decay": 1e-3,
|
|
}
|
|
|
|
model_config = DictConfig(
|
|
{
|
|
"preprocessor": DictConfig(preprocessor),
|
|
"spec_augment": DictConfig(spec_augment),
|
|
'model_defaults': DictConfig(model_defaults),
|
|
"masking": DictConfig(masking),
|
|
"quantizer": DictConfig(quantizer),
|
|
"encoder": DictConfig(encoder),
|
|
"decoder": DictConfig(decoder),
|
|
"loss": DictConfig(loss),
|
|
"optim": DictConfig(optim),
|
|
}
|
|
)
|
|
ssl_model = EncDecDenoiseMaskedTokenPredModel(cfg=model_config)
|
|
return ssl_model
|
|
|
|
|
|
class TestSSLModel:
|
|
@pytest.mark.unit
|
|
def test_constructor(self, ssl_model):
|
|
confdict = ssl_model.to_config_dict()
|
|
instance2 = SpeechEncDecSelfSupervisedModel.from_config_dict(confdict)
|
|
assert isinstance(instance2, SpeechEncDecSelfSupervisedModel)
|
|
|
|
@pytest.mark.unit
|
|
def test_contr_nonquant(self, ssl_model):
|
|
modelConfig_contr_nonquant = ssl_model.to_config_dict()
|
|
|
|
loss_list_contr_nonquant = dict(modelConfig_contr_nonquant['loss_list'])
|
|
del loss_list_contr_nonquant['mlm']
|
|
|
|
loss_list_contr_nonquant['contr']['loss']['quantized_targets'] = False
|
|
|
|
modelConfig_contr_nonquant['loss_list'] = DictConfig(loss_list_contr_nonquant)
|
|
|
|
ssl_model = SpeechEncDecSelfSupervisedModel(cfg=modelConfig_contr_nonquant)
|
|
|
|
input_signal = torch.randn(size=(4, 64000))
|
|
length = torch.randint(low=48000, high=64000, size=[4])
|
|
|
|
with torch.no_grad():
|
|
spectrograms, spec_masks, encoded, encoded_len = ssl_model.forward(
|
|
input_signal=input_signal, input_signal_length=length
|
|
)
|
|
|
|
loss_value, loss_val_dict = ssl_model.decoder_loss_step(spectrograms, spec_masks, encoded, encoded_len)
|
|
|
|
assert len(loss_val_dict) == 1
|
|
|
|
@pytest.mark.unit
|
|
def test_contr_mlm(self, ssl_model):
|
|
|
|
input_signal = torch.randn(size=(4, 64000))
|
|
length = torch.randint(low=48000, high=64000, size=[4])
|
|
|
|
with torch.no_grad():
|
|
spectrograms, spec_masks, encoded, encoded_len = ssl_model.forward(
|
|
input_signal=input_signal, input_signal_length=length
|
|
)
|
|
|
|
loss_value, loss_val_dict = ssl_model.decoder_loss_step(spectrograms, spec_masks, encoded, encoded_len)
|
|
|
|
assert len(loss_val_dict) == 2
|
|
|
|
@pytest.mark.unit
|
|
def test_contr_mlm_multi(self, ssl_model):
|
|
modelConfig_contr_mlm_multi = ssl_model.to_config_dict()
|
|
|
|
model_defaults = modelConfig_contr_mlm_multi['model_defaults']
|
|
|
|
loss_list_contr_mlm_multi = dict(modelConfig_contr_mlm_multi['loss_list'])
|
|
loss_list_contr_mlm_multi['mlm_2'] = {
|
|
'decoder': {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASRDecoder',
|
|
'feat_in': model_defaults['enc_hidden'],
|
|
'num_classes': 4096,
|
|
},
|
|
'loss': {'_target_': 'nemo.collections.asr.losses.MLMLoss', 'combine_time_steps': 1},
|
|
'output_from_layer': "encoder.0",
|
|
'targets_from_loss': "contr",
|
|
}
|
|
loss_list_contr_mlm_multi['mlm_3'] = {
|
|
'decoder': {
|
|
'_target_': 'nemo.collections.asr.modules.ConvASRDecoder',
|
|
'feat_in': model_defaults['enc_hidden'],
|
|
'num_classes': 4096,
|
|
},
|
|
'loss': {'_target_': 'nemo.collections.asr.losses.MLMLoss', 'combine_time_steps': 1},
|
|
'output_from_layer': "encoder.1",
|
|
'targets_from_loss': "contr",
|
|
}
|
|
modelConfig_contr_mlm_multi['loss_list'] = DictConfig(loss_list_contr_mlm_multi)
|
|
|
|
ssl_model = SpeechEncDecSelfSupervisedModel(cfg=modelConfig_contr_mlm_multi)
|
|
|
|
input_signal = torch.randn(size=(4, 64000))
|
|
length = torch.randint(low=48000, high=64000, size=[4])
|
|
|
|
with torch.no_grad():
|
|
spectrograms, spec_masks, encoded, encoded_len = ssl_model.forward(
|
|
input_signal=input_signal, input_signal_length=length
|
|
)
|
|
|
|
loss_value, loss_val_dict = ssl_model.decoder_loss_step(spectrograms, spec_masks, encoded, encoded_len)
|
|
|
|
assert len(loss_val_dict) == 4
|
|
|
|
|
|
class TestContrastiveLoss:
|
|
@pytest.mark.unit
|
|
def test_sample_negatives_fewer_frames_than_num_negatives(self):
|
|
num_negatives = 40
|
|
num_frames = 5
|
|
num = num_frames
|
|
feat_dim = 128
|
|
|
|
loss = ContrastiveLoss(in_dim=64, proj_dim=feat_dim, num_negatives=num_negatives, quantized_targets=False)
|
|
y = torch.randn(num_frames, feat_dim)
|
|
|
|
negs, neg_idxs = loss.sample_negatives(y, num)
|
|
|
|
assert neg_idxs.shape == (num, num_negatives)
|
|
assert negs.shape == (num_negatives, num, feat_dim)
|
|
|
|
|
|
class TestDenoiseMLMSSLModel:
|
|
@pytest.mark.unit
|
|
def test_forward(self, denoise_mlm_ssl_model):
|
|
input_signal = torch.randn(size=(4, 64000))
|
|
input_length = torch.randint(low=48000, high=64000, size=[4])
|
|
noise = 0.1 * torch.ones_like(input_signal)
|
|
noisy_input_signal = input_signal + noise
|
|
noisy_input_length = input_length
|
|
with torch.no_grad():
|
|
with typecheck.disable_checks():
|
|
log_probs, encoded_len, masks, tokens = denoise_mlm_ssl_model.forward(
|
|
input_signal=input_signal,
|
|
input_signal_length=input_length,
|
|
noisy_input_signal=noisy_input_signal,
|
|
noisy_input_signal_length=noisy_input_length,
|
|
)
|
|
|
|
assert log_probs.size(0) == 4
|
|
assert log_probs.size(2) == denoise_mlm_ssl_model.cfg.model_defaults.num_classes
|
|
assert encoded_len.size(0) == 4
|
|
assert masks.size(0) == 4
|
|
assert tokens.size(0) == 4
|
|
assert masks.sum() == 0.0 # no mask should be applied to the input by default
|
|
|
|
@pytest.mark.unit
|
|
def test_forward_masked(self, denoise_mlm_ssl_model: EncDecDenoiseMaskedTokenPredModel):
|
|
input_signal = torch.randn(size=(4, 64000))
|
|
input_length = torch.randint(low=48000, high=64000, size=[4])
|
|
noise = 0.1 * torch.ones_like(input_signal)
|
|
noisy_input_signal = input_signal + noise
|
|
noisy_input_length = input_length
|
|
|
|
with torch.no_grad():
|
|
with typecheck.disable_checks():
|
|
log_probs, encoded_len, masks, tokens = denoise_mlm_ssl_model.forward(
|
|
input_signal=input_signal,
|
|
input_signal_length=input_length,
|
|
noisy_input_signal=noisy_input_signal,
|
|
noisy_input_signal_length=noisy_input_length,
|
|
apply_mask=True,
|
|
)
|
|
|
|
loss_value = denoise_mlm_ssl_model.loss(
|
|
masks=masks, decoder_outputs=log_probs, targets=tokens, decoder_lengths=encoded_len
|
|
)
|
|
|
|
assert log_probs.size(0) == 4
|
|
assert log_probs.size(2) == denoise_mlm_ssl_model.cfg.model_defaults.num_classes
|
|
assert encoded_len.size(0) == 4
|
|
assert masks.size(0) == 4
|
|
assert tokens.size(0) == 4
|
|
assert masks.sum() > 0.0 # mask should be applied to the input
|
|
assert not torch.isnan(loss_value)
|