softcatala/whisper-medium-ca
316
1#!/usr/bin/env python2# coding=utf-83# Copyright 2022 The HuggingFace Team. All rights reserved.4#5# Licensed under the Apache License, Version 2.0 (the "License");6# you may not use this file except in compliance with the License.7# You may obtain a copy of the License at8#9# http://www.apache.org/licenses/LICENSE-2.010#11# Unless required by applicable law or agreed to in writing, software12# distributed under the License is distributed on an "AS IS" BASIS,13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14# See the License for the specific language governing permissions and15# limitations under the License.16"""17Fine-tuning the library models for sequence to sequence speech recognition18with ๐ค Datasets' streaming mode.19"""20# You can also adapt this script for your own sequence to sequence speech21# recognition task. Pointers for this are left as comments.22 23import logging24import os25import sys26from dataclasses import dataclass, field27from typing import Any, Dict, List, Optional, Union28 29import datasets30import torch31from datasets import DatasetDict, IterableDatasetDict, interleave_datasets, load_dataset32from torch.utils.data import IterableDataset33 34import evaluate35import transformers36from transformers import (37 AutoConfig,38 AutoFeatureExtractor,39 AutoModelForSpeechSeq2Seq,40 AutoProcessor,41 AutoTokenizer,42 HfArgumentParser,43 Seq2SeqTrainer,44 Seq2SeqTrainingArguments,45 TrainerCallback,46 set_seed,47)48from transformers.models.whisper.english_normalizer import BasicTextNormalizer49from transformers.trainer_pt_utils import IterableDatasetShard50from transformers.trainer_utils import get_last_checkpoint, is_main_process51from transformers.utils import check_min_version, send_example_telemetry52from transformers.utils.versions import require_version53 54 55# Will error if the minimal version of Transformers is not installed. Remove at your own risks.56check_min_version("4.25.0.dev0")57 58require_version("datasets>=1.18.2", "To fix: pip install -r examples/pytorch/speech-recognition/requirements.txt")59 60logger = logging.getLogger(__name__)61 62 63@dataclass64class ModelArguments:65 """66 Arguments pertaining to which model/config/tokenizer we are going to fine-tune from.67 """68 69 model_name_or_path: str = field(70 metadata={"help": "Path to pretrained model or model identifier from huggingface.co/models"}71 )72 config_name: Optional[str] = field(73 default=None, metadata={"help": "Pretrained config name or path if not the same as model_name"}74 )75 tokenizer_name: Optional[str] = field(76 default=None, metadata={"help": "Pretrained tokenizer name or path if not the same as model_name"}77 )78 feature_extractor_name: Optional[str] = field(79 default=None, metadata={"help": "feature extractor name or path if not the same as model_name"}80 )81 cache_dir: Optional[str] = field(82 default=None,83 metadata={"help": "Where to store the pretrained models downloaded from huggingface.co"},84 )85 use_fast_tokenizer: bool = field(86 default=True,87 metadata={"help": "Whether to use one of the fast tokenizer (backed by the tokenizers library) or not."},88 )89 model_revision: str = field(90 default="main",91 metadata={"help": "The specific model version to use (can be a branch name, tag name or commit id)."},92 )93 use_auth_token: bool = field(94 default=False,95 metadata={96 "help": (97 "Will use the token generated when running `huggingface-cli login` (necessary to use this script "98 "with private models)."99 )100 },101 )102 freeze_feature_encoder: bool = field(103 default=True, metadata={"help": "Whether to freeze the feature encoder layers of the model."}104 )105 freeze_encoder: bool = field(106 default=False, metadata={"help": "Whether to freeze the entire encoder of the seq2seq model."}107 )108 forced_decoder_ids: List[List[int]] = field(109 default=None,110 metadata={111 "help": (112 "A list of pairs of integers which indicates a mapping from generation indices to token indices "113 "that will be forced before sampling. For example, [[0, 123]] means the first generated token "114 "will always be a token of index 123."115 )116 },117 )118 suppress_tokens: List[int] = field(119 default=None, metadata={"help": "A list of tokens that will be suppressed at generation."}120 )121 model_index_name: str = field(default=None, metadata={"help": "Pretty name for the model card."})122 123 124@dataclass125class DataTrainingArguments:126 """127 Arguments pertaining to what data we are going to input our model for training and eval.128 """129 130 dataset_name: str = field(131 default=None, metadata={"help": "The name of the dataset to use (via the datasets library)."}132 )133 dataset_config_name: Optional[str] = field(134 default=None, metadata={"help": "The configuration name of the dataset to use (via the datasets library)."}135 )136 text_column: Optional[str] = field(137 default=None,138 metadata={"help": "The name of the column in the datasets containing the full texts (for summarization)."},139 )140 max_train_samples: Optional[int] = field(141 default=None,142 metadata={143 "help": (144 "For debugging purposes or quicker training, truncate the number of training examples to this "145 "value if set."146 )147 },148 )149 max_eval_samples: Optional[int] = field(150 default=None,151 metadata={152 "help": (153 "For debugging purposes or quicker training, truncate the number of evaluation examples to this "154 "value if set."155 )156 },157 )158 audio_column_name: str = field(159 default="audio",160 metadata={"help": "The name of the dataset column containing the audio data. Defaults to 'audio'"},161 )162 text_column_name: str = field(163 default="text",164 metadata={"help": "The name of the dataset column containing the text data. Defaults to 'text'"},165 )166 max_duration_in_seconds: float = field(167 default=20.0,168 metadata={169 "help": (170 "Truncate audio files that are longer than `max_duration_in_seconds` seconds to"171 " 'max_duration_in_seconds`"172 )173 },174 )175 min_duration_in_seconds: float = field(176 default=0.0, metadata={"help": "Filter audio files that are shorter than `min_duration_in_seconds` seconds"}177 )178 train_split_name: str = field(179 default="train",180 metadata={181 "help": "The name of the training data set split to use (via the datasets library). Defaults to 'train'"182 },183 )184 eval_split_name: str = field(185 default="test",186 metadata={187 "help": "The name of the training data set split to use (via the datasets library). Defaults to 'train'"188 },189 )190 do_lower_case: bool = field(191 default=False,192 metadata={"help": "Whether the target text should be lower cased."},193 )194 do_remove_punctuation: bool = field(195 default=False,196 metadata={"help": "Whether the target text should be striped of punctuation."},197 )198 do_normalize_eval: bool = field(199 default=True,200 metadata={"help": "Whether to normalise the references and predictions in the eval WER calculation."},201 )202 language: str = field(203 default=None,204 metadata={205 "help": (206 "Language for multilingual fine-tuning. This argument should be set for multilingual fine-tuning "207 "only. For English speech recognition, it should be set to `None`."208 )209 },210 )211 task: str = field(212 default="transcribe",213 metadata={"help": "Task, either `transcribe` for speech recognition or `translate` for speech translation."},214 )215 shuffle_buffer_size: Optional[int] = field(216 default=500,217 metadata={218 "help": (219 "The number of streamed examples to download before shuffling them. The large the buffer, "220 "the closer it is to real offline shuffling."221 )222 },223 )224 streaming: bool = field(225 default=True,226 metadata={"help": "Whether to use streaming mode to load and pre-process the data."},227 )228 229 230@dataclass231class DataCollatorSpeechSeq2SeqWithPadding:232 """233 Data collator that will dynamically pad the inputs received.234 Args:235 processor ([`WhisperProcessor`])236 The processor used for processing the data.237 decoder_start_token_id (`int`)238 The begin-of-sentence of the decoder.239 """240 241 processor: Any242 decoder_start_token_id: int243 244 def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:245 # split inputs and labels since they have to be of different lengths and need246 # different padding methods247 model_input_name = self.processor.model_input_names[0]248 input_features = [{model_input_name: feature[model_input_name]} for feature in features]249 label_features = [{"input_ids": feature["labels"]} for feature in features]250 251 batch = self.processor.feature_extractor.pad(input_features, return_tensors="pt")252 253 labels_batch = self.processor.tokenizer.pad(label_features, return_tensors="pt")254 255 # replace padding with -100 to ignore loss correctly256 labels = labels_batch["input_ids"].masked_fill(labels_batch.attention_mask.ne(1), -100)257 258 # if bos token is appended in previous tokenization step,259 # cut bos token here as it's append later anyways260 if (labels[:, 0] == self.decoder_start_token_id).all().cpu().item():261 labels = labels[:, 1:]262 263 batch["labels"] = labels264 265 return batch266 267 268def load_maybe_streaming_dataset(dataset_name, dataset_config_name, split="train", streaming=True, **kwargs):269 """270 Utility function to load a dataset in streaming mode. For datasets with multiple splits,271 each split is loaded individually and then splits combined by taking alternating examples from272 each (interleaving).273 """274 if "+" in split:275 # load multiple splits separated by the `+` symbol with streaming mode276 dataset_splits = [277 load_dataset(dataset_name, dataset_config_name, split=split_name, streaming=streaming, **kwargs)278 for split_name in split.split("+")279 ]280 # interleave multiple splits to form one dataset281 interleaved_dataset = interleave_datasets(dataset_splits)282 return interleaved_dataset283 else:284 # load a single split *with* streaming mode285 dataset = load_dataset(dataset_name, dataset_config_name, split=split, streaming=streaming, **kwargs)286 return dataset287 288 289def main():290 # 1. Parse input arguments291 # See all possible arguments in src/transformers/training_args.py292 # or by passing the --help flag to this script.293 # We now keep distinct sets of args, for a cleaner separation of concerns.294 parser = HfArgumentParser((ModelArguments, DataTrainingArguments, Seq2SeqTrainingArguments))295 296 if len(sys.argv) == 2 and sys.argv[1].endswith(".json"):297 # If we pass only one argument to the script and it's the path to a json file,298 # let's parse it to get our arguments.299 model_args, data_args, training_args = parser.parse_json_file(json_file=os.path.abspath(sys.argv[1]))300 else:301 model_args, data_args, training_args = parser.parse_args_into_dataclasses()302 303 # Sending telemetry. Tracking the example usage helps us better allocate resources to maintain them. The304 # information sent is the one passed as arguments along with your Python/PyTorch versions.305 send_example_telemetry("run_speech_recognition_seq2seq_streaming", model_args, data_args)306 307 # 2. Setup logging308 logging.basicConfig(309 format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",310 datefmt="%m/%d/%Y %H:%M:%S",311 handlers=[logging.StreamHandler(sys.stdout)],312 )313 log_level = training_args.get_process_log_level()314 logger.setLevel(log_level)315 datasets.utils.logging.set_verbosity(log_level)316 transformers.utils.logging.set_verbosity(log_level)317 transformers.utils.logging.enable_default_handler()318 transformers.utils.logging.enable_explicit_format()319 320 logger.setLevel(logging.INFO if is_main_process(training_args.local_rank) else logging.WARN)321 322 # Log on each process the small summary:323 logger.warning(324 f"Process rank: {training_args.local_rank}, device: {training_args.device}, n_gpu: {training_args.n_gpu}"325 f"distributed training: {bool(training_args.local_rank != -1)}, 16-bits training: {training_args.fp16}"326 )327 logger.info(f"Training/evaluation parameters {training_args}")328 329 # Set the verbosity to info of the Transformers logger (on main process only):330 if is_main_process(training_args.local_rank):331 transformers.utils.logging.set_verbosity_info()332 logger.info("Training/evaluation parameters %s", training_args)333 334 # 3. Detecting last checkpoint and eventually continue from last checkpoint335 last_checkpoint = None336 if os.path.isdir(training_args.output_dir) and training_args.do_train and not training_args.overwrite_output_dir:337 last_checkpoint = get_last_checkpoint(training_args.output_dir)338 if last_checkpoint is None and len(os.listdir(training_args.output_dir)) > 0:339 raise ValueError(340 f"Output directory ({training_args.output_dir}) already exists and is not empty. "341 "Use --overwrite_output_dir to overcome."342 )343 elif last_checkpoint is not None and training_args.resume_from_checkpoint is None:344 logger.info(345 f"Checkpoint detected, resuming training at {last_checkpoint}. To avoid this behavior, change "346 "the `--output_dir` or add `--overwrite_output_dir` to train from scratch."347 )348 349 # Set seed before initializing model.350 set_seed(training_args.seed)351 352 # 4. Load dataset353 raw_datasets = IterableDatasetDict() if data_args.streaming else DatasetDict()354 355 if training_args.do_train:356 raw_datasets["train"] = load_maybe_streaming_dataset(357 data_args.dataset_name,358 data_args.dataset_config_name,359 split=data_args.train_split_name,360 use_auth_token=True if model_args.use_auth_token else None,361 streaming=data_args.streaming,362 )363 364 if training_args.do_eval:365 raw_datasets["eval"] = load_maybe_streaming_dataset(366 data_args.dataset_name,367 data_args.dataset_config_name,368 split=data_args.eval_split_name,369 use_auth_token=True if model_args.use_auth_token else None,370 streaming=data_args.streaming,371 )372 373 raw_datasets_features = list(next(iter(raw_datasets.values())).features.keys())374 375 if data_args.audio_column_name not in raw_datasets_features:376 raise ValueError(377 f"--audio_column_name '{data_args.audio_column_name}' not found in dataset '{data_args.dataset_name}'. "378 "Make sure to set `--audio_column_name` to the correct audio column - one of "379 f"{', '.join(raw_datasets_features)}."380 )381 382 if data_args.text_column_name not in raw_datasets_features:383 raise ValueError(384 f"--text_column_name {data_args.text_column_name} not found in dataset '{data_args.dataset_name}'. "385 "Make sure to set `--text_column_name` to the correct text column - one of "386 f"{', '.join(raw_datasets_features)}."387 )388 389 # 5. Load pretrained model, tokenizer, and feature extractor390 #391 # Distributed training:392 # The .from_pretrained methods guarantee that only one local process can concurrently393 config = AutoConfig.from_pretrained(394 model_args.config_name if model_args.config_name else model_args.model_name_or_path,395 cache_dir=model_args.cache_dir,396 revision=model_args.model_revision,397 use_auth_token=True if model_args.use_auth_token else None,398 )399 400 config.update({"forced_decoder_ids": model_args.forced_decoder_ids, "suppress_tokens": model_args.suppress_tokens})401 402 if training_args.gradient_checkpointing:403 config.update({"use_cache": False})404 405 feature_extractor = AutoFeatureExtractor.from_pretrained(406 model_args.feature_extractor_name if model_args.feature_extractor_name else model_args.model_name_or_path,407 cache_dir=model_args.cache_dir,408 revision=model_args.model_revision,409 use_auth_token=True if model_args.use_auth_token else None,410 )411 tokenizer = AutoTokenizer.from_pretrained(412 model_args.tokenizer_name if model_args.tokenizer_name else model_args.model_name_or_path,413 cache_dir=model_args.cache_dir,414 use_fast=model_args.use_fast_tokenizer,415 revision=model_args.model_revision,416 use_auth_token=True if model_args.use_auth_token else None,417 )418 model = AutoModelForSpeechSeq2Seq.from_pretrained(419 model_args.model_name_or_path,420 config=config,421 cache_dir=model_args.cache_dir,422 revision=model_args.model_revision,423 use_auth_token=True if model_args.use_auth_token else None,424 )425 426 if model.config.decoder_start_token_id is None:427 raise ValueError("Make sure that `config.decoder_start_token_id` is correctly defined")428 429 if model_args.freeze_feature_encoder:430 model.freeze_feature_encoder()431 432 if model_args.freeze_encoder:433 model.freeze_encoder()434 435 if data_args.language is not None:436 # We only need to set the task id when the language is specified (i.e. in a multilingual setting)437 tokenizer.set_prefix_tokens(language=data_args.language, task=data_args.task)438 439 # 6. Resample speech dataset if necessary440 dataset_sampling_rate = next(iter(raw_datasets.values())).features[data_args.audio_column_name].sampling_rate441 if dataset_sampling_rate != feature_extractor.sampling_rate:442 raw_datasets = raw_datasets.cast_column(443 data_args.audio_column_name, datasets.features.Audio(sampling_rate=feature_extractor.sampling_rate)444 )445 446 # 7. Preprocessing the datasets.447 # We need to read the audio files as arrays and tokenize the targets.448 max_input_length = data_args.max_duration_in_seconds * feature_extractor.sampling_rate449 min_input_length = data_args.min_duration_in_seconds * feature_extractor.sampling_rate450 audio_column_name = data_args.audio_column_name451 text_column_name = data_args.text_column_name452 model_input_name = feature_extractor.model_input_names[0]453 do_lower_case = data_args.do_lower_case454 do_remove_punctuation = data_args.do_remove_punctuation455 normalizer = BasicTextNormalizer() # 'official' text normalizer from OpenAI456 457 if data_args.max_train_samples is not None:458 raw_datasets["train"] = (459 raw_datasets["train"].take(data_args.max_train_samples)460 if data_args.streaming461 else raw_datasets["train"].select(range(data_args.max_train_samples))462 )463 464 if data_args.max_eval_samples is not None:465 raw_datasets["eval"] = (466 raw_datasets["eval"].take(data_args.max_eval_samples)467 if data_args.streaming468 else raw_datasets["eval"].select(range(data_args.max_eval_samples))469 )470 471 def prepare_dataset(batch):472 # process audio473 sample = batch[audio_column_name]474 inputs = feature_extractor(sample["array"], sampling_rate=sample["sampling_rate"])475 # process audio length476 batch[model_input_name] = inputs.get(model_input_name)[0]477 batch["input_length"] = len(sample["array"])478 479 # process targets480 input_str = batch[text_column_name].lower() if do_lower_case else batch[text_column_name]481 if do_remove_punctuation:482 input_str = normalizer(input_str).strip()483 batch["labels"] = tokenizer(input_str).input_ids484 return batch485 486 with training_args.main_process_first(desc="dataset map pre-processing"):487 vectorized_datasets = raw_datasets.map(488 prepare_dataset,489 remove_columns=raw_datasets_features,490 ).with_format("torch")491 492 if training_args.do_train and data_args.streaming:493 # manually shuffle if streaming (done by the trainer for non-streaming)494 vectorized_datasets["train"] = vectorized_datasets["train"].shuffle(495 buffer_size=data_args.shuffle_buffer_size,496 seed=training_args.seed,497 )498 499 # filter training data that is shorter than min_input_length or longer than500 # max_input_length501 def is_audio_in_length_range(length):502 return min_input_length < length < max_input_length503 504 if training_args.do_train:505 vectorized_datasets["train"] = vectorized_datasets["train"].filter(506 is_audio_in_length_range,507 input_columns=["input_length"],508 )509 510 # 8. Load Metric511 metric = evaluate.load("wer")512 do_normalize_eval = data_args.do_normalize_eval513 514 def compute_metrics(pred):515 pred_ids = pred.predictions516 517 pred.label_ids[pred.label_ids == -100] = tokenizer.pad_token_id518 519 pred_str = tokenizer.batch_decode(pred_ids, skip_special_tokens=True)520 # we do not want to group tokens when computing the metrics521 label_str = tokenizer.batch_decode(pred.label_ids, skip_special_tokens=True)522 523 if do_normalize_eval:524 pred_str = [normalizer(pred) for pred in pred_str]525 label_str = [normalizer(label) for label in label_str]526 # filtering step to only evaluate the samples that correspond to non-zero references:527 pred_str = [pred_str[i] for i in range(len(pred_str)) if len(label_str[i]) > 0]528 label_str = [label_str[i] for i in range(len(label_str)) if len(label_str[i]) > 0]529 530 wer = 100 * metric.compute(predictions=pred_str, references=label_str)531 532 return {"wer": wer}533 534 # 9. Create a single speech processor535 if is_main_process(training_args.local_rank):536 # save feature extractor, tokenizer and config537 feature_extractor.save_pretrained(training_args.output_dir)538 tokenizer.save_pretrained(training_args.output_dir)539 config.save_pretrained(training_args.output_dir)540 541 processor = AutoProcessor.from_pretrained(training_args.output_dir)542 543 # 10. Define data collator544 data_collator = DataCollatorSpeechSeq2SeqWithPadding(545 processor=processor,546 decoder_start_token_id=model.config.decoder_start_token_id,547 )548 549 # 11. Configure Trainer550 # Trainer callback to reinitialise and reshuffle the streamable datasets at the beginning of each epoch551 # Only required for streaming: Trainer automatically shuffles non-streaming datasets552 class ShuffleCallback(TrainerCallback):553 def on_epoch_begin(self, args, state, control, train_dataloader, **kwargs):554 if isinstance(train_dataloader.dataset, IterableDatasetShard):555 pass # set_epoch() is handled by the Trainer556 elif isinstance(train_dataloader.dataset, IterableDataset):557 train_dataloader.dataset.set_epoch(train_dataloader.dataset._epoch + 1)558 559 # Initialize Trainer560 trainer = Seq2SeqTrainer(561 model=model,562 args=training_args,563 train_dataset=vectorized_datasets["train"] if training_args.do_train else None,564 eval_dataset=vectorized_datasets["eval"] if training_args.do_eval else None,565 tokenizer=feature_extractor,566 data_collator=data_collator,567 compute_metrics=compute_metrics if training_args.predict_with_generate else None,568 callbacks=[ShuffleCallback()] if data_args.streaming else None,569 )570 571 # 12. Training572 if training_args.do_train:573 checkpoint = None574 if training_args.resume_from_checkpoint is not None:575 checkpoint = training_args.resume_from_checkpoint576 elif last_checkpoint is not None:577 checkpoint = last_checkpoint578 train_result = trainer.train(resume_from_checkpoint=checkpoint)579 trainer.save_model() # Saves the feature extractor too for easy upload580 581 metrics = train_result.metrics582 if data_args.max_train_samples:583 metrics["train_samples"] = data_args.max_train_samples584 trainer.log_metrics("train", metrics)585 trainer.save_metrics("train", metrics)586 trainer.save_state()587 588 # 13. Evaluation589 results = {}590 if training_args.do_eval:591 logger.info("*** Evaluate ***")592 metrics = trainer.evaluate(593 metric_key_prefix="eval",594 max_length=training_args.generation_max_length,595 num_beams=training_args.generation_num_beams,596 )597 if data_args.max_eval_samples:598 metrics["eval_samples"] = data_args.max_eval_samples599 600 trainer.log_metrics("eval", metrics)601 trainer.save_metrics("eval", metrics)602 603 # 14. Write Training Stats604 kwargs = {605 "finetuned_from": model_args.model_name_or_path,606 "tasks": "automatic-speech-recognition",607 "tags": "whisper-event",608 }609 if data_args.dataset_name is not None:610 kwargs["dataset_tags"] = data_args.dataset_name611 if data_args.dataset_config_name is not None:612 kwargs["dataset"] = f"{data_args.dataset_name} {data_args.dataset_config_name}"613 else:614 kwargs["dataset"] = data_args.dataset_name615 if "common_voice" in data_args.dataset_name:616 kwargs["language"] = data_args.dataset_config_name[:2]617 if model_args.model_index_name is not None:618 kwargs["model_name"] = model_args.model_index_name619 620 if training_args.push_to_hub:621 trainer.push_to_hub(**kwargs)622 else:623 trainer.create_model_card(**kwargs)624 625 return results626 627 628if __name__ == "__main__":629 main()630 