Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/main/examples/speech_recognition.ipynb
Views: 2535
Fine-tuning Speech Model with 🤗 Transformers
This notebook shows how to fine-tune multi-lingual pretrained speech models for Automatic Speech Recognition.
This notebook is built to run on the TIMIT dataset with any speech model checkpoint from the Model Hub as long as that model has a version with a Connectionist Temporal Classification (CTC) head. Depending on the model and the GPU you are using, you might need to adjust the batch size to avoid out-of-memory errors. Set those two parameters, then the rest of the notebook should run smoothly:
For a more in-detail explanation of how multi-lingual pretrained speech models function, please take a look at the 🤗 Blog.
Before we start, let's install both datasets
and transformers
from master. Also, we need the librosa
package to load audio files and the jiwer
to evaluate our fine-tuned model using the word error rate (WER) metric .
Next we strongly suggest to upload your training checkpoints directly to the 🤗 Hub while training. The 🤗 Hub has integrated version control so you can be sure that no model checkpoint is getting lost during training.
To do so you have to store your authentication token from the Hugging Face website (sign up here if you haven't already!)
Then you need to install Git-LFS to upload your model checkpoints:
Timit is usually evaluated using the phoneme error rate (PER), but by far the most common metric in ASR is the word error rate (WER). To keep this notebook as general as possible we decided to evaluate the model using WER.
We also quickly upload some telemetry - this tells us which examples and software versions are getting used so we know where to prioritize our maintenance efforts. We don't collect (or care about) any personally identifiable information, but if you'd prefer not to be counted, feel free to skip this step or delete this cell entirely.
Prepare Data, Tokenizer, Feature Extractor
ASR models transcribe speech to text, which means that we both need a feature extractor that processes the speech signal to the model's input format, e.g. a feature vector, and a tokenizer that processes the model's output format to text.
In 🤗 Transformers, speech recognition models are thus accompanied by both a tokenizer, and a feature extractor.
Let's start by creating the tokenizer responsible for decoding the model's predictions.
Create Wav2Vec2CTCTokenizer
Let's start by loading the TIMIT dataset and taking a look at its structure.
If you wish to fine-tune the model on a different speech dataset feel free to adapt this part.
Many ASR datasets only provide the target text, 'text'
for each audio 'audio'
and file 'file'
. Timit actually provides much more information about each audio file, such as the 'phonetic_detail'
, etc., which is why many researchers choose to evaluate their models on phoneme classification instead of speech recognition when working with Timit. However, we want to keep the notebook as general as possible, so that we will only consider the transcribed text for fine-tuning.
Let's write a short function to display some random samples of the dataset and run it a couple of times to get a feeling for the transcriptions.
Alright! The transcriptions look very clean and the language seems to correspond more to written text than dialogue. This makes sense taking into account that Timit is a read speech corpus.
We can see that the transcriptions contain some special characters, such as ,.?!;:
. Without a language model, it is much harder to classify speech chunks to such special characters because they don't really correspond to a characteristic sound unit. E.g., the letter "s"
has a more or less clear sound, whereas the special character "."
does not. Also in order to understand the meaning of a speech signal, it is usually not necessary to include special characters in the transcription.
In addition, we normalize the text to only have lower case letters and append a word separator token at the end.
Good! This looks better. We have removed most special characters from transcriptions and normalized them to lower-case only.
In CTC, it is common to classify speech chunks into letters, so we will do the same here. Let's extract all distinct letters of the training and test data and build our vocabulary from this set of letters.
We write a mapping function that concatenates all transcriptions into one long transcription and then transforms the string into a set of chars. It is important to pass the argument batched=True
to the map(...)
function so that the mapping function has access to all transcriptions at once.
Now, we create the union of all distinct letters in the training dataset and test dataset and convert the resulting list into an enumerated dictionary.
Cool, we see that all letters of the alphabet occur in the dataset (which is not really surprising) and we also extracted the special characters " "
and '
. Note that we did not exclude those special characters because:
The model has to learn to predict when a word finished or else the model prediction would always be a sequence of chars which would make it impossible to separate words from each other.
In English, we need to keep the
'
character to differentiate between words, e.g.,"it's"
and"its"
which have very different meanings.
To make it clearer that " "
has its own token class, we give it a more visible character |
. In addition, we also add an "unknown" token so that the model can later deal with characters not encountered in Timit's training set.
Finally, we also add a padding token that corresponds to CTC's "blank token". The "blank token" is a core component of the CTC algorithm. For more information, please take a look at the "Alignment" section here.
Cool, now our vocabulary is complete and consists of 30 tokens, which means that the linear layer that we will add on top of the pretrained speech checkpoint will have an output dimension of 30.
Let's now save the vocabulary as a json file.
In a final step, we use the json file to instantiate a tokenizer object with the just created vocabulary file. The correct tokenizer_type
can be retrieved from the model configuration. If a tokenizer_class
is defined in the config, we can use it, else we assume the tokenizer_type
corresponds to the model_type
.
Now we can instantiate a tokenizer using AutoTokenizer
. Additionally, we set the tokenizer's special tokens.
If one wants to re-use the just created tokenizer with the fine-tuned model of this notebook, it is strongly advised to upload the tokenizer
to the 🤗 Hub. Let's call the repo to which we will upload the files "wav2vec2-base-timit-demo-colab"
:
and upload the tokenizer to the 🤗 Hub.
Great, you can see the just created repository under https://huggingface.co/<your-username>/wav2vec2-base-timit-demo-colab
Preprocess Data
So far, we have not looked at the actual values of the speech signal but just the transcription. In addition to 'text'
, our datasets include two more column names 'file'
and 'audio'
. 'file'
states the absolute path of the audio file. Let's take a look.
Wav2Vec2
expects the input in the format of a 1-dimensional array of 16 kHz. This means that the audio file has to be loaded and resampled.
Thankfully, datasets
does this automatically when calling the column audio
. Let try it out.
We can see that the audio file has automatically been loaded. This is thanks to the new "Audio"
feature introduced in datasets == 1.13.3
, which loads and resamples audio files on-the-fly upon calling.
The sampling rate is set to 16kHz which is what Wav2Vec2
expects as an input.
Great, let's listen to a couple of audio files to better understand the dataset and verify that the audio was correctly loaded.
Note: You can click the following cell a couple of times to listen to different speech samples.
It can be heard, that the speakers change along with their speaking rate, accent, etc. Overall, the recordings sound relatively clear though, which is to be expected from a read speech corpus.
Let's do a final check that the data is correctly prepared, by printing the shape of the speech input, its transcription, and the corresponding sampling rate.
Note: You can click the following cell a couple of times to verify multiple samples.
Good! Everything looks fine - the data is a 1-dimensional array, the sampling rate always corresponds to 16kHz, and the target text is normalized.
Next, we should process the data with the model's feature extractor. Let's load the feature extractor
and wrap it into a Wav2Vec2Processor together with the tokenizer.
Finally, we can leverage Wav2Vec2Processor
to process the data to the format expected by the model for training. To do so let's make use of Dataset's map(...)
function.
First, we load and resample the audio data, simply by calling batch["audio"]
. Second, we extract the input_values
from the loaded audio file. In our case, the Wav2Vec2Processor
only normalizes the data. For other speech models, however, this step can include more complex feature extraction, such as Log-Mel feature extraction. Third, we encode the transcriptions to label ids.
Let's apply the data preparation function to all examples.
Note: Currently datasets
make use of torchaudio
and librosa
for audio loading and resampling. If you wish to implement your own costumized data loading/sampling, feel free to just make use of the "path"
column instead and disregard the "audio"
column.
Long input sequences require a lot of memory. Since Wav2Vec2
is based on self-attention
the memory requirement scales quadratically with the input length for long input sequences (cf. with this reddit post). For this demo, let's filter all sequences that are longer than 4 seconds out of the training dataset.
Awesome, now we are ready to start training!
Training
The data is processed so that we are ready to start setting up the training pipeline. We will make use of 🤗's Trainer for which we essentially need to do the following:
Define a data collator. In contrast to most NLP models, speech models usually have a much larger input length than output length. E.g., a sample of input length 50000 for Wav2Vec2 has an output length of no more than 100. Given the large input sizes, it is much more efficient to pad the training batches dynamically meaning that all training samples should only be padded to the longest sample in their batch and not the overall longest sample. Therefore, fine-tuning speech models requires a special padding data collator, which we will define below
Evaluation metric. During training, the model should be evaluated on the word error rate. We should define a
compute_metrics
function accordinglyLoad a pretrained checkpoint. We need to load a pretrained checkpoint and configure it correctly for training.
Define the training configuration.
After having fine-tuned the model, we will correctly evaluate it on the test data and verify that it has indeed learned to correctly transcribe speech.
Set-up Trainer
Let's start by defining the data collator. The code for the data collator was copied from this example.
Without going into too many details, in contrast to the common data collators, this data collator treats the input_values
and labels
differently and thus applies to separate padding functions on them. This is necessary because in speech input and output are of different modalities meaning that they should not be treated by the same padding function. Analogous to the common data collators, the padding tokens in the labels with -100
so that those tokens are not taken into account when computing the loss.
Next, the evaluation metric is defined. As mentioned earlier, the predominant metric in ASR is the word error rate (WER), hence we will use it in this notebook as well.
The model will return a sequence of logit vectors: with and .
A logit vector contains the log-odds for each word in the vocabulary we defined earlier, thus config.vocab_size
. We are interested in the most likely prediction of the model and thus take the argmax(...)
of the logits. Also, we transform the encoded labels back to the original string by replacing -100
with the pad_token_id
and decoding the ids while making sure that consecutive tokens are not grouped to the same token in CTC style .
Now, we can load the pretrained Wav2Vec2
checkpoint. The tokenizer's pad_token_id
must be to define the model's pad_token_id
or in the case of a CTC speech model also CTC's blank token .
The first component of most transformer-based speech models consists of a stack of CNN layers that are used to extract acoustically meaningful - but contextually independent - features from the raw speech signal. This part of the model has already been sufficiently trained during pretraining and as stated in the paper does not need to be fine-tuned anymore. Thus, we can set the requires_grad
to False
for all parameters of the feature extraction part.
In a final step, we define all parameters related to training. To give more explanation on some of the parameters:
group_by_length
makes training more efficient by grouping training samples of similar input length into one batch. This can significantly speed up training time by heavily reducing the overall number of useless padding tokens that are passed through the modellearning_rate
andweight_decay
were heuristically tuned until fine-tuning has become stable. Note that those parameters strongly depend on the Timit dataset and might be suboptimal for other speech datasets.
For more explanations on other parameters, one can take a look at the docs.
During training, a checkpoint will be uploaded asynchronously to the hub every 400 training steps. It allows you to also play around with the demo widget even while your model is still training.
Note: If one does not want to upload the model checkpoints to the hub, simply set push_to_hub=False
.
Now, all instances can be passed to Trainer and we are ready to start training!
To allow models to become independent of the speaker rate, in CTC, consecutive tokens that are identical are simply grouped as a single token. However, the encoded labels should not be grouped when decoding since they don't correspond to the predicted tokens of the model, which is why the group_tokens=False
parameter has to be passed. If we wouldn't pass this parameter a word like "hello"
would incorrectly be encoded, and decoded as "helo"
.
The blank token allows the model to predict a word, such as "hello"
by forcing it to insert the blank token between the two l's. A CTC-conform prediction of "hello"
of our model would be [PAD] [PAD] "h" "e" "e" "l" "l" [PAD] "l" "o" "o" [PAD]
.
Training
Training will take a couple of hours depending on the GPU allocated to this notebook.
The final WER should be around 0.3 which is reasonable given that state-of-the-art phoneme error rates (PER) are just below 0.1 (see leaderboard) and that WER is usually worse than PER.
You can now upload the result of the training to the Hub, just execute this instruction:
You can now share this model with all your friends, family, favorite pets: they can all load it with the identifier "your-username/the-name-you-picked" so for instance:
To fine-tune larger models on larger datasets using CTC loss, one should take a look at the official speech-recognition examples here 🤗.