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
Fix M-CTC-T chunking #18949
base: main
Are you sure you want to change the base?
Fix M-CTC-T chunking #18949
Conversation
| @@ -30,18 +30,33 @@ | |||
| from ..models.auto.modeling_auto import MODEL_FOR_CTC_MAPPING, MODEL_FOR_SPEECH_SEQ_2_SEQ_MAPPING | |||
|
|
|||
|
|
|||
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.
tl;dr of changes to this function:
This function takes a list of strides stride. It previously took a single number ratio. Now, it takes a list of numbers ratio to match the list of strides. This lets the ratio differ between strides.
| attention_mask = model_inputs.pop("attention_mask", None) | ||
| outputs = self.model(input_values=input_values, attention_mask=attention_mask) | ||
| outputs = self.model(input_values, attention_mask=attention_mask) | ||
| logits = outputs.logits | ||
|
|
||
| if self.type == "ctc_with_lm": | ||
| out = {"logits": logits} | ||
| else: | ||
| out = {"tokens": logits.argmax(dim=-1)} | ||
| if stride is not None: |
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.
Here is the main change. The previous approach of calculating the inputs_to_logits_ratio from the convolutional layers of the model is still used when possible. For M-CTC-T, essentially the new approach is:
- Calculate the ratio of the original length of the input features (
stride[0]) divided by the length of the output logits (outputs.logits.shape[1]) - Assume that this ratio holds for the strides
| @@ -377,6 +379,33 @@ def test_chunking_fast(self): | |||
| self.assertEqual(output, [{"text": ANY(str)}]) | |||
| self.assertEqual(output[0]["text"][:6], "ZBT ZC") | |||
|
|
|||
| @require_torch | |||
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.
Heads up: this new test uses speechbrain/m-ctc-t-large, which is a 4.1 GB model.
I'm not familiar with the testing infrastructure, so if that's undesirable there may be other options for testing. However, there is no readily available official/popular "smaller" version of this model.
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.
This should be fine thanks :-)
| outputs = self.model(input_values=input_values, attention_mask=attention_mask) | ||
| outputs = self.model(input_values, attention_mask=attention_mask) |
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.
M-CTC-T takes an argument input_features instead of input_values, so I made this a positional argument instead of a keyword argument. I'm happy to add some if/else logic to keep it a keyword argument if desired, but this is a bit easier to read.
| # And also for multiple batched inputs | ||
| n_repeats = 2 | ||
| audio_tiled = np.tile(audio, n_repeats) | ||
| output = speech_recognizer([audio_tiled], batch_size=2) | ||
| self.assertEqual(output, [{"text": ANY(str)}]) | ||
| self.assertEqual(output[0]["text"][:26], "A man said to the universe") |
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.
Flagging this because I can't for the life of me understand why the other ASR tests are constructed this way:
n_repeats = 2
audio_tiled = np.tile(audio, n_repeats)
output = speech_recognizer([audio_tiled], batch_size=2)If audio is an array with shape (N,), then audio_tiled is an array with shape (2 * N,), and output is a list of length 1. It seems to me like a correct test for batched inputs would instead look like:
output = speech_recognizer([audio, audio], batch_size=2)Which indeed works and produces an output which is length 2. Am I crazy?
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.
Hmm - I'm not 100% sure here - was the idea to test a long input? cc'ing @Narsil here - was the test intended?
| model = MCTCTForCTC.from_pretrained("speechbrain/m-ctc-t-large") | ||
| processor = MCTCTProcessor.from_pretrained("speechbrain/m-ctc-t-large") | ||
| speech_recognizer = AutomaticSpeechRecognitionPipeline( | ||
| feature_extractor=processor.feature_extractor, | ||
| tokenizer=processor.tokenizer, | ||
| model=model, | ||
| chunk_length_s=10.0, | ||
| framework="pt", | ||
| ) |
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.
On a side note, I wasn't able to load this model with pipeline directly because there is a typo in one of the model artifacts: https://huggingface.co/speechbrain/m-ctc-t-large/blob/main/preprocessor_config.json
It's MCTCFeatureExtractor instead of MCTCTFeatureExtractor (missing the T).
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.
Thanks for the info - updating!
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. |
|
In addition to running tests, as a sanity check I ran the code snippet from this blog post: from transformers import pipeline
pipe = pipeline(model="facebook/wav2vec2-base-960h")
pipe("very_long_file.mp3", chunk_length_s=10)and it works as expected. I also modified it for M-CTC-T and it still works: from transformers import AutomaticSpeechRecognitionPipeline, MCTCTForCTC, MCTCTProcessor
model = MCTCTForCTC.from_pretrained("speechbrain/m-ctc-t-large")
processor = MCTCTProcessor.from_pretrained("speechbrain/m-ctc-t-large")
pipe = AutomaticSpeechRecognitionPipeline(
feature_extractor=processor.feature_extractor,
tokenizer=processor.tokenizer,
model=model,
framework="pt",
)
print(pipe("very_long_file.mp3", chunk_length_s=10)) |
|
Thanks for the PR - this all looks good to me! cc @sanchit-gandhi and @Narsil for a quick second review :-) |
| @@ -258,7 +273,7 @@ def preprocess(self, inputs, chunk_length_s=0, stride_length_s=None): | |||
| # XXX: Carefuly, this variable will not exist in `seq2seq` setting. | |||
| # Currently chunking is not possible at this level for `seq2seq` so | |||
| # it's ok. | |||
| align_to = self.model.config.inputs_to_logits_ratio | |||
| align_to = getattr(self.model.config, "inputs_to_logits_ratio", 1) | |||
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.
@Narsil ok for you or should we add 1 to the model's config?
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 personally wouldn't add 1 to the config because we actually don't know what the "inputs to logits ratio" is. In other models like wav2vec, inputs_to_logits_ratio is a property that calculates the product of the strides of the convolutional layers, but we can't do that with M-CTC-T. As far as I can tell, it needs to be calculated dynamically.
This is assigning align_to, which is used on the subsequent lines to try and make sure that the chunk length and stride lengths are even multiples of that ratio. But in the case we don't know what that ratio is yet, setting align_to = 1 simply aligns the chunk length and stride lengths to the nearest frame instead.
| # Shape is [B, SEQ] for tokens | ||
| # [B, SEQ, V] for logits | ||
|
|
||
| new_strides = [] | ||
| for input_n, left, right in stride: | ||
| token_n = int(round(input_n * ratio)) | ||
| for (input_n, left, right), stride_ratio in zip(stride, ratio): |
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.
Ok for me! Thanks for making it backward compatible. @Narsil what do you think?
| # If the proportion of input feature frames to logit frames can be statically | ||
| # determined based on convolution strides, use that. Otherwise, we need to | ||
| # calculate it here dynamically. | ||
| inputs_to_logits_ratio = getattr(self.model.config, "inputs_to_logits_ratio", None) |
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.
Hmm this is a bit inconsistent with the line above where the default is 1 and not None
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.
@Narsil could you take a look here?
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.
Replied to your comment above to clarify the inconsistency but basically:
- Above we're assigning
align_to, and if we don't know the inputs-to-logits-ratio,1is the only reasonable choice (that I can think of) - But here we're actually rescaling the strides, and so if we don't know the inputs-to-logits-ratio, we'll need to calculate it
|
Thanks for the PR - that's a great addition! Generally this all looks more or less all good to me! Just a bit unsure about the changes in the pipeline, but ok for me since all tests pass. @Narsil what do you think? Also cc @sanchit-gandhi for info |
|
Thanks for the PR @samwaterbury! LGTM - happy with the changes to computing the inputs : logits ratio :-) |
|
@Narsil if you have 10 minutes it'd be super nice to get your review here :-) |
|
Thanks all! |
Summary
Chunking doesn't currently work for the M-CTC-T architecture, which this PR attempts to fix. The change is fairly minor but I am not an expert on how M-CTC-T works, so definitely open to feedback. In my usage, it works as intended.
Paging @patrickvonplaten since I think you originally implemented the chunking mechanism.🙂
I will add some PR comments explaining the changes.
Testing
I added one test to cover the ASR pipeline with M-CTC-T. I also ran these tests:
RUN_SLOW=True RUN_PIPELINE_TESTS=True pytest \ tests/models/mctct \ tests/pipelines/test_pipelines_automatic_speech_recognition.py \ tests/pipelines/test_pipelines_common.pySome of these tests fail, but I was able to confirm they're also failing on the main branch. Here are the failures:
Some of these look to just be issues with my machine (memory errors).