-
Notifications
You must be signed in to change notification settings - Fork 33.9k
[WIP] [Whisper] Add specaugment #21063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
55eb49d
1fff680
8eae1a9
9e8050e
a627523
0875d00
957d323
0888740
24abf6b
3699d71
0f95e69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ | |
| Feature extractor class for Whisper | ||
| """ | ||
|
|
||
| from typing import List, Optional, Union | ||
| from typing import List, Optional, Tuple, Union | ||
|
|
||
| import numpy as np | ||
| from numpy.fft import fft | ||
|
|
@@ -29,6 +29,126 @@ | |
| logger = logging.get_logger(__name__) | ||
|
|
||
|
|
||
| # Copied from transformers.models.wav2vec2.modeling_wav2vec2._compute_mask_indices with attention_mask from torch.LongTensor to np.array | ||
| def _compute_mask_indices( | ||
| shape: Tuple[int, int], | ||
| mask_prob: float, | ||
| mask_length: int, | ||
| attention_mask: Optional[np.array] = None, | ||
| min_masks: int = 0, | ||
| ) -> np.ndarray: | ||
| """ | ||
| Computes random mask spans for a given shape. Used to implement [SpecAugment: A Simple Data Augmentation Method for | ||
| ASR](https://arxiv.org/abs/1904.08779). Note that this method is not optimized to run on TPU and should be run on | ||
| CPU as part of the preprocessing during training. | ||
|
|
||
| Args: | ||
| shape: The shape for which to compute masks. This should be of a tuple of size 2 where | ||
| the first element is the batch size and the second element is the length of the axis to span. | ||
| mask_prob: The percentage of the whole axis (between 0 and 1) which will be masked. The number of | ||
| independently generated mask spans of length `mask_length` is computed by | ||
| `mask_prob*shape[1]/mask_length`. Note that due to overlaps, `mask_prob` is an upper bound and the | ||
| actual percentage will be smaller. | ||
| mask_length: size of the mask | ||
| min_masks: minimum number of masked spans | ||
| attention_mask: A (right-padded) attention mask which independently shortens the feature axis of | ||
| each batch dimension. | ||
| """ | ||
| batch_size, sequence_length = shape | ||
|
|
||
| if mask_length < 1: | ||
| raise ValueError("`mask_length` has to be bigger than 0.") | ||
|
|
||
| if mask_length > sequence_length: | ||
| raise ValueError( | ||
| f"`mask_length` has to be smaller than `sequence_length`, but got `mask_length`: {mask_length}" | ||
| f" and `sequence_length`: {sequence_length}`" | ||
| ) | ||
|
|
||
| # epsilon is used for probabilistic rounding | ||
| epsilon = np.random.rand(1).item() | ||
|
|
||
| def compute_num_masked_span(input_length): | ||
| """Given input length, compute how many spans should be masked""" | ||
| num_masked_span = int(mask_prob * input_length / mask_length + epsilon) | ||
| num_masked_span = max(num_masked_span, min_masks) | ||
|
|
||
| # make sure num masked span <= sequence_length | ||
| if num_masked_span * mask_length > sequence_length: | ||
| num_masked_span = sequence_length // mask_length | ||
|
|
||
| # make sure num_masked span is also <= input_length - (mask_length - 1) | ||
| if input_length - (mask_length - 1) < num_masked_span: | ||
| num_masked_span = max(input_length - (mask_length - 1), 0) | ||
|
|
||
| return num_masked_span | ||
|
|
||
| # compute number of masked spans in batch | ||
| input_lengths = ( | ||
| attention_mask.sum(-1).tolist() | ||
| if attention_mask is not None | ||
| else [sequence_length for _ in range(batch_size)] | ||
| ) | ||
|
|
||
| # SpecAugment mask to fill | ||
| spec_aug_mask = np.zeros((batch_size, sequence_length), dtype=bool) | ||
| spec_aug_mask_idxs = [] | ||
|
|
||
| max_num_masked_span = compute_num_masked_span(sequence_length) | ||
|
|
||
| if max_num_masked_span == 0: | ||
| return spec_aug_mask | ||
|
|
||
| for input_length in input_lengths: | ||
| # compute num of masked spans for this input | ||
| num_masked_span = compute_num_masked_span(input_length) | ||
|
|
||
| # get random indices to mask | ||
| spec_aug_mask_idx = np.random.choice( | ||
| np.arange(input_length - (mask_length - 1)), num_masked_span, replace=False | ||
| ) | ||
|
|
||
| # pick first sampled index that will serve as a dummy index to pad vector | ||
| # to ensure same dimension for all batches due to probabilistic rounding | ||
| # Picking first sample just pads those vectors twice. | ||
| if len(spec_aug_mask_idx) == 0: | ||
| # this case can only happen if `input_length` is strictly smaller then | ||
| # `sequence_length` in which case the last token has to be a padding | ||
| # token which we can use as a dummy mask id | ||
| dummy_mask_idx = sequence_length - 1 | ||
| else: | ||
| dummy_mask_idx = spec_aug_mask_idx[0] | ||
|
|
||
| spec_aug_mask_idx = np.concatenate( | ||
| [spec_aug_mask_idx, np.ones(max_num_masked_span - num_masked_span, dtype=np.int32) * dummy_mask_idx] | ||
| ) | ||
| spec_aug_mask_idxs.append(spec_aug_mask_idx) | ||
|
|
||
| spec_aug_mask_idxs = np.array(spec_aug_mask_idxs) | ||
|
|
||
| # expand masked indices to masked spans | ||
| spec_aug_mask_idxs = np.broadcast_to( | ||
| spec_aug_mask_idxs[:, :, None], (batch_size, max_num_masked_span, mask_length) | ||
| ) | ||
| spec_aug_mask_idxs = spec_aug_mask_idxs.reshape(batch_size, max_num_masked_span * mask_length) | ||
|
|
||
| # add offset to the starting indexes so that indexes now create a span | ||
| offsets = np.arange(mask_length)[None, None, :] | ||
| offsets = np.broadcast_to(offsets, (batch_size, max_num_masked_span, mask_length)).reshape( | ||
| batch_size, max_num_masked_span * mask_length | ||
| ) | ||
| spec_aug_mask_idxs = spec_aug_mask_idxs + offsets | ||
|
|
||
| # ensure that we cannot have indices larger than sequence_length | ||
| if spec_aug_mask_idxs.max() > sequence_length - 1: | ||
| spec_aug_mask_idxs[spec_aug_mask_idxs > sequence_length - 1] = sequence_length - 1 | ||
|
|
||
| # scatter indices to mask | ||
| np.put_along_axis(spec_aug_mask, spec_aug_mask_idxs, 1, -1) | ||
|
|
||
| return spec_aug_mask | ||
|
|
||
|
|
||
| class WhisperFeatureExtractor(SequenceFeatureExtractor): | ||
| r""" | ||
| Constructs a Whisper feature extractor. | ||
|
|
@@ -215,6 +335,59 @@ def _np_extract_fbank_features(self, waveform: np.array) -> np.ndarray: | |
|
|
||
| return log_spec | ||
|
|
||
| # Modified from transformers.models.wav2vec2.modeling_wav2vec2.Wav2Vec2Model._mask_hidden_states | ||
| def _mask_input_features( | ||
| self, | ||
| input_features: List[np.array], | ||
| mask_time_indices: Optional[np.array] = None, | ||
| attention_mask: Optional[np.array] = None, | ||
| ): | ||
| """ | ||
| Masks extracted features along time axis and/or along feature axis according to | ||
| [SpecAugment](https://arxiv.org/abs/1904.08779). | ||
| """ | ||
|
|
||
| # generate indices & apply SpecAugment along time axis | ||
| batch_size, hidden_size, sequence_length = input_features.shape | ||
|
|
||
| # todo: move to config | ||
| self.mask_time_prob = 0.05 | ||
| self.mask_time_length = 2 | ||
| self.mask_time_min_masks = 2 | ||
|
|
||
| self.mask_feature_prob = 0.05 | ||
| self.mask_feature_length = 10 | ||
| self.mask_feature_min_masks = 0 | ||
|
|
||
| if mask_time_indices is not None: | ||
| # apply SpecAugment along time axis with given mask_time_indices | ||
| input_features[mask_time_indices] = 0 | ||
| elif self.mask_time_prob > 0: | ||
| # generate indices & apply SpecAugment along time axis | ||
| mask_time_indices = _compute_mask_indices( | ||
| (batch_size, sequence_length), | ||
| mask_prob=self.mask_time_prob, | ||
| mask_length=self.mask_time_length, | ||
| attention_mask=attention_mask, | ||
| min_masks=self.mask_time_min_masks, | ||
| ) | ||
| mask_time_indices = np.broadcast_to(mask_time_indices[:, None], (batch_size, hidden_size, sequence_length)) | ||
| # mask_time_indices = np.tile(mask_time_indices, (1, hidden_size, 1)) | ||
| # mask_time_indices = np.repeat(mask_time_indices[:, None, :], hidden_size, axis=1) | ||
| input_features[mask_time_indices] = 0 | ||
|
|
||
| if self.mask_feature_prob > 0: | ||
| # generate indices & apply SpecAugment along feature axis | ||
| mask_feature_indices = _compute_mask_indices( | ||
| (batch_size, hidden_size), | ||
| mask_prob=self.mask_feature_prob, | ||
| mask_length=self.mask_feature_length, | ||
| min_masks=self.mask_feature_min_masks, | ||
| ) | ||
| input_features[mask_feature_indices] = 0 | ||
|
|
||
| return input_features | ||
|
|
||
| def __call__( | ||
| self, | ||
| raw_speech: Union[np.ndarray, List[float], List[np.ndarray], List[List[float]]], | ||
|
|
@@ -301,23 +474,37 @@ def __call__( | |
|
|
||
| # convert into correct format for padding | ||
|
|
||
| # todo: auto return_attention_mask | ||
| padded_inputs = self.pad( | ||
| batched_speech, | ||
| padding=padding, | ||
| max_length=max_length if max_length else self.n_samples, | ||
| truncation=truncation, | ||
| pad_to_multiple_of=pad_to_multiple_of, | ||
| return_attention_mask=True, | ||
| ) | ||
| # make sure list is in array format | ||
| input_features = padded_inputs.get("input_features").transpose(2, 0, 1) | ||
|
|
||
| # mono | ||
| input_features = [self._np_extract_fbank_features(waveform) for waveform in input_features[0]] | ||
|
|
||
| if isinstance(input_features[0], List): | ||
| padded_inputs["input_features"] = [np.asarray(feature, dtype=np.float32) for feature in input_features] | ||
| else: | ||
| padded_inputs["input_features"] = input_features | ||
|
|
||
| # todo: move to config | ||
| apply_spec_augment = True | ||
| if apply_spec_augment: | ||
| # todo: input_features to np array | ||
| padded_inputs["input_features"] = np.stack(padded_inputs["input_features"], 0) | ||
|
|
||
| padded_inputs["input_features"] = self._mask_input_features( | ||
| padded_inputs["input_features"], | ||
| attention_mask=padded_inputs.attention_mask[:, ::self.hop_length], | ||
| ) | ||
|
Comment on lines
+503
to
+549
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make sure that we don't change the return type here! (previously was either a List or a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Could we make
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @ArthurZucker , I think this PR is about to finish except this. What's your opinion here :) |
||
|
|
||
| if return_tensors is not None: | ||
| padded_inputs = padded_inputs.convert_to_tensors(return_tensors) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also add the function to the
model_dox/whisper.mdxto have it appear in the documentation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I completed docstring of
_mask_input_featuresand added it tomodel_dox/whisper.mdx