| |
| |
| |
| |
| @@ -1,13 +1,18 @@ |
| torch==2.2.0 |
| torchvision==0.17.0 |
| torchaudio==2.2.0 |
| -soundfile |
| +sentencepiece==0.2.1 |
| numpy==1.26.4 |
| + |
| +datasets |
| +editdistance |
| +soundfile |
| torch_audiomentations |
| tqdm |
| matplotlib |
| pandas |
| wandb |
| +wget |
| comet_ml |
| hydra-core |
| |
| |
| |
| |
| |
| @@ -1,33 +1,33 @@ |
| defaults: |
| - - model: baseline |
| - - writer: wandb |
| + - model: conformer |
| + - writer: cometml |
| - metrics: example |
| - datasets: example |
| - dataloader: example |
| - transforms: example_only_instance |
| + - text_encoder: ctc_text_encoder |
| - _self_ |
| optimizer: |
| _target_: torch.optim.AdamW |
| lr: 3e-4 |
| lr_scheduler: |
| _target_: torch.optim.lr_scheduler.OneCycleLR |
| - max_lr: 1e-2 |
| + max_lr: 1e-4 |
| pct_start: 0.1 |
| - steps_per_epoch: ${trainer.epoch_len} |
| epochs: ${trainer.n_epochs} |
| + steps_per_epoch: null |
| anneal_strategy: cos |
| loss_function: |
| _target_: src.loss.CTCLossWrapper |
| -text_encoder: |
| - _target_: src.text_encoder.CTCTextEncoder |
| + zero_infinity: True |
| trainer: |
| log_step: 50 |
| - n_epochs: 50 |
| - epoch_len: 200 |
| + n_epochs: 10 |
| + epoch_len: null |
| device_tensors: ["spectrogram", "text_encoded"] # which tensors should be on device (ex. GPU) |
| resume_from: null # null or path to the checkpoint dir with *.pth and config.yaml |
| device: auto # device name or "auto" |
| - override: False # if True, will override the previous run with the same name |
| + override: True # if True, will override the previous run with the same name |
| monitor: "min val_WER_(Argmax)" # "off" or "max/min metric_name", i.e. our goal is to maximize/minimize metric |
| save_period: 5 # checkpoint each save_period epochs in addition to the best epoch |
| early_stop: ${trainer.n_epochs} # epochs for early stopping |
| |
| |
| |
| |
| @@ -1,4 +1,4 @@ |
| _target_: torch.utils.data.DataLoader |
| -batch_size: 10 |
| +batch_size: 16 |
| num_workers: 2 |
| pin_memory: True |
| |
| |
| |
| |
| @@ -5,8 +5,6 @@ train: |
| max_text_length: 200 |
| limit: 2 |
| instance_transforms: ${transforms.instance_transforms.train} |
| -# we filter partitions in one batch test to check the pipeline |
| -# do not filter test dataset, you want to evaluate on the whole dataset |
| val: |
| _target_: src.datasets.LibrispeechDataset |
| part: "dev-clean" |
| |
| |
| |
| |
| @@ -1,13 +1,14 @@ |
| defaults: |
| - - model: baseline |
| - - metrics: example |
| - - datasets: example_eval # we do not want to run inference on training data |
| + - model: conformer |
| + - metrics: beam_lm |
| + - datasets: eval |
| - dataloader: example |
| - - transforms: example |
| + - transforms: example_only_instance |
| + - text_encoder: ctc_text_encoder |
| - _self_ |
| inferencer: |
| - device_tensors: ["data_object", "labels"] # which tensors should be on device (ex. GPU) |
| + device_tensors: ["spectrogram", "text_encoded"] # which tensors should be on device (ex. GPU) |
| device: auto # device name or "auto" |
| save_path: "example" # any name here, can be a dataset name |
| seed: 1 |
| - from_pretrained: "saved/model_best.pth" # path to the pretrained model |
| + from_pretrained: "saved/beam_search_big_model/model_best.pth" # path to the pretrained model |
| |
| |
| |
| |
| @@ -1,17 +1,40 @@ |
| import torch |
| +from torch.nn.utils.rnn import pad_sequence |
| |
| |
| def collate_fn(dataset_items: list[dict]): |
| - """ |
| - Collate and pad fields in the dataset items. |
| - Converts individual items into a batch. |
| - |
| - Args: |
| - dataset_items (list[dict]): list of objects from |
| - dataset.__getitem__. |
| - Returns: |
| - result_batch (dict[Tensor]): dict, containing batch-version |
| - of the tensors. |
| - """ |
| - |
| - pass # TODO |
| + audios = [it["audio"].squeeze(0) for it in dataset_items] |
| + specs = [it["spectrogram"] for it in dataset_items] |
| + texts = [it["text"] for it in dataset_items] |
| + paths = [it["audio_path"] for it in dataset_items] |
| + |
| + txt_ids = [it["text_encoded"].squeeze(0).to(dtype=torch.long) |
| + for it in dataset_items] |
| + |
| + audio_len = torch.tensor([a.numel() for a in audios], dtype=torch.long) |
| + spec_len = torch.tensor([s.shape[-1] for s in specs], dtype=torch.long) |
| + txt_len = torch.tensor([t.numel() for t in txt_ids], dtype=torch.long) |
| + |
| + audio_batch = pad_sequence(audios, batch_first=True, padding_value=0.0) |
| + |
| + specs_TF = [s.squeeze(0).transpose(0, 1) for s in specs] |
| + specs_padded = pad_sequence(specs_TF, batch_first=True, padding_value=0.0) |
| + spec_batch = specs_padded.transpose(1, 2).contiguous() |
| + |
| + text_encoded_batch = pad_sequence(txt_ids, batch_first=True, padding_value=0) |
| + |
| + batch = { |
| + "audio": audio_batch.float(), |
| + "audio_length": audio_len, |
| + |
| + "spectrogram": spec_batch.float(), |
| + "spectrogram_length": spec_len, |
| + |
| + "text": texts, |
| + "text_encoded": text_encoded_batch, |
| + "text_encoded_length": txt_len, |
| + |
| + "audio_path": paths, |
| + } |
| + |
| + return batch |
| |
| |
| |
| |
| @@ -12,7 +12,9 @@ from src.utils.io_utils import ROOT_PATH |
| |
| class CommonVoiceDataset(BaseDataset): |
| def __init__(self, split, *args, **kwargs): |
| - self._data_dir = ROOT_PATH / "dataset_common_voice" |
| + self._data_dir = ROOT_PATH / "data"/ "datasets"/ "common_voice" |
| + self._data_dir.mkdir(exist_ok=True, parents=True) |
| + |
| self._regex = re.compile("[^a-z ]") |
| self._dataset = load_dataset( |
| "mozilla-foundation/common_voice_11_0", |
| |
| |
| |
| |
| @@ -62,6 +62,7 @@ class CometMLWriter: |
| exp_class = comet_ml.Experiment |
| |
| self.exp = exp_class( |
| + "XlK5HKtwRja1CCxz4bK6lfjCQ", |
| project_name=project_name, |
| workspace=workspace, |
| experiment_key=self.run_id, |
| |
| |
| |
| |
| @@ -60,7 +60,7 @@ def plot_spectrogram(spectrogram, name=None): |
| buf.seek(0) |
| |
| # convert buffer to Tensor |
| - image = ToTensor()(PIL.Image.open(buf)) |
| + image = ToTensor()(PIL.Image.open(buf).convert("RGB")) |
| |
| plt.close() |
| |
| |
| |
| |
| |
| @@ -1,2 +1,2 @@ |
| -from src.metrics.cer import ArgmaxCERMetric |
| -from src.metrics.wer import ArgmaxWERMetric |
| +from src.metrics.cer import ArgmaxCERMetric, BeamSearchCERMetric, CustomBeamSearchCERMetric |
| +from src.metrics.wer import ArgmaxWERMetric, BeamSearchWERMetric, CustomBeamSearchWERMetric |
| |
| |
| |
| |
| @@ -1,4 +1,12 @@ |
| from abc import abstractmethod |
| +import torch |
| +from torch import Tensor |
| +from typing import List |
| + |
| +from src.utils.io_utils import ROOT_PATH |
| +from src.metrics.utils import expand_and_merge_beams, truncate_beams |
| + |
| +from pyctcdecode import build_ctcdecoder |
| |
| |
| class BaseMetric: |
| @@ -20,3 +28,99 @@ class BaseMetric: |
| Can use external functions (like TorchMetrics) or custom ones. |
| """ |
| raise NotImplementedError() |
| + |
| + |
| +class ArgmaxMetric(BaseMetric): |
| + def __init__(self, text_encoder, binary_func=None, *args, **kwargs): |
| + super().__init__(*args, **kwargs) |
| + self.text_encoder = text_encoder |
| + self.binary_func = binary_func |
| + |
| + def __call__( |
| + self, log_probs: Tensor, log_probs_length: Tensor, text: List[str], **kwargs |
| + ): |
| + results = [] |
| + predictions = torch.argmax(log_probs.detach().cpu(), dim=-1).numpy() |
| + lengths = log_probs_length.detach().numpy() |
| + for log_prob_vec, length, target_text in zip(predictions, lengths, text): |
| + target_text = self.text_encoder.normalize_text(target_text) |
| + pred_text = self.text_encoder.ctc_decode(log_prob_vec[:length]) |
| + results.append(self.binary_func(target_text, pred_text)) |
| + return sum(results) / len(results) |
| + |
| + |
| +class BeamSearchMetric(BaseMetric): |
| + def __init__(self, text_encoder, beam_size=20, binary_func=None, *args, **kwargs): |
| + super().__init__(*args, **kwargs) |
| + self.text_encoder = text_encoder |
| + self.beam_size = beam_size |
| + self.EMPTY_TOK = text_encoder.EMPTY_TOK |
| + self.binary_func = binary_func |
| + |
| + ind2char = {i: self.text_encoder[i] for i in range(len(self.text_encoder))} |
| + VOCAB = [ind2char[i] for i in range(len(ind2char))] |
| + |
| + ctcdecoder_args = {} |
| + if "use_lm" in kwargs and kwargs["use_lm"]: |
| + from torchaudio.models.decoder import download_pretrained_files |
| + files = download_pretrained_files("librispeech-4-gram") |
| + ctcdecoder_args["kenlm_model_path"] = files.lm |
| + |
| + if "alpha" in kwargs: |
| + ctcdecoder_args["alpha"] = kwargs["alpha"] |
| + if "beta" in kwargs: |
| + ctcdecoder_args["beta"] = kwargs["beta"] |
| + |
| + self.decoder = build_ctcdecoder(VOCAB, **ctcdecoder_args) |
| + |
| + def __call__( |
| + self, log_probs: Tensor, log_probs_length: Tensor, text: List[str], **kwargs, |
| + ): |
| + results = [] |
| + for i, (T, target_text) in enumerate(zip(log_probs_length.detach().cpu().tolist(), text)): |
| + logits = log_probs[i, :T, :].detach().cpu().numpy() |
| + pred_text = self.decoder.decode(logits, beam_width=self.beam_size) |
| + |
| + if getattr(self.text_encoder, "SPACE_PIECE", False): |
| + pred_text = pred_text.replace(self.text_encoder.SPACE_PIECE, " ") |
| + |
| + ref = self.text_encoder.normalize_text(target_text) |
| + results.append(self.binary_func(ref, pred_text)) |
| + return sum(results) / len(results) |
| + |
| + |
| +class CustomBeamSearchMetric(BaseMetric): |
| + def __init__(self, text_encoder, beam_size=20, binary_func=None, *args, **kwargs): |
| + super().__init__(*args, **kwargs) |
| + self.text_encoder = text_encoder |
| + self.beam_size = beam_size |
| + self.EMPTY_TOK = text_encoder.EMPTY_TOK |
| + self.binary_func = binary_func |
| + |
| + ind2char = {i: self.text_encoder[i] for i in range(len(self.text_encoder))} |
| + self.VOCAB = [ind2char[i] for i in range(len(ind2char))] |
| + |
| + def __call__( |
| + self, log_probs: Tensor, log_probs_length: Tensor, text: List[str], **kwargs, |
| + ): |
| + results = [] |
| + |
| + for i, (T, target_text) in enumerate(zip(log_probs_length.detach().cpu().tolist(), text)): |
| + probs = log_probs[i, :T, :].detach().cpu().exp() |
| + dp = {("", self.EMPTY_TOK): 1.0} |
| + for t in range(T): |
| + cur_step_prob = probs[t] |
| + dp = expand_and_merge_beams(dp, cur_step_prob, self.VOCAB, self.EMPTY_TOK) |
| + dp = truncate_beams(dp, self.beam_size) |
| + |
| + hypos = [(pref, proba) for (pref, _), proba in dp.items()] |
| + hypos.sort(key=lambda x: -x[1]) |
| + pred_text = hypos[0][0] if hypos else "" |
| + |
| + if getattr(self.text_encoder, "SPACE_PIECE", False): |
| + pred_text = pred_text.replace(self.text_encoder.SPACE_PIECE, " ") |
| + |
| + ref = self.text_encoder.normalize_text(target_text) |
| + results.append(self.binary_func(ref, pred_text)) |
| + |
| + return sum(results) / len(results) |
| |
| |
| |
| |
| @@ -1,29 +1,17 @@ |
| -from typing import List |
| +from src.metrics.base_metric import ArgmaxMetric, BeamSearchMetric, CustomBeamSearchMetric |
| +from src.metrics.utils import calc_cer |
| |
| -import torch |
| -from torch import Tensor |
| |
| -from src.metrics.base_metric import BaseMetric |
| -from src.metrics.utils import calc_cer |
| +class ArgmaxCERMetric(ArgmaxMetric): |
| + def __init__(self, *args, **kwargs): |
| + super().__init__(*args, binary_func=calc_cer, **kwargs) |
| |
| -# TODO add beam search/lm versions |
| -# Note: they can be written in a pretty way |
| -# Note 2: overall metric design can be significantly improved |
| |
| +class BeamSearchCERMetric(BeamSearchMetric): |
| + def __init__(self, *args, **kwargs): |
| + super().__init__(*args, binary_func=calc_cer, **kwargs) |
| |
| -class ArgmaxCERMetric(BaseMetric): |
| - def __init__(self, text_encoder, *args, **kwargs): |
| - super().__init__(*args, **kwargs) |
| - self.text_encoder = text_encoder |
| |
| - def __call__( |
| - self, log_probs: Tensor, log_probs_length: Tensor, text: List[str], **kwargs |
| - ): |
| - cers = [] |
| - predictions = torch.argmax(log_probs.cpu(), dim=-1).numpy() |
| - lengths = log_probs_length.detach().numpy() |
| - for log_prob_vec, length, target_text in zip(predictions, lengths, text): |
| - target_text = self.text_encoder.normalize_text(target_text) |
| - pred_text = self.text_encoder.ctc_decode(log_prob_vec[:length]) |
| - cers.append(calc_cer(target_text, pred_text)) |
| - return sum(cers) / len(cers) |
| +class CustomBeamSearchCERMetric(CustomBeamSearchMetric): |
| + def __init__(self, *args, **kwargs): |
| + super().__init__(*args, binary_func=calc_cer, **kwargs) |
| |
| |
| |
| |
| @@ -1,13 +1,44 @@ |
| -# Based on seminar materials |
| +import editdistance |
| +from collections import defaultdict |
| +import tempfile |
| +import subprocess |
| |
| -# Don't forget to support cases when target_text == '' |
| +from src.utils.io_utils import ROOT_PATH |
| + |
| + |
| +def calc_wer(target_text, predicted_text) -> float: |
| + if not target_text: |
| + return 1 if predicted_text else 0 |
| + return editdistance.eval(target_text.split(), predicted_text.split()) / len(target_text.split()) |
| |
| |
| def calc_cer(target_text, predicted_text) -> float: |
| - # TODO |
| - pass |
| + if not target_text: |
| + return 1 if predicted_text else 0 |
| + return editdistance.eval(target_text, predicted_text) / len(target_text) |
| |
| |
| -def calc_wer(target_text, predicted_text) -> float: |
| - # TODO |
| - pass |
| +def expand_and_merge_beams(dp, cur_step_prob, vocab, empty_tok): |
| + new_dp = defaultdict(float) |
| + |
| + for (pref, prev_char), pref_proba in dp.items(): |
| + for idx, char in enumerate(vocab): |
| + cur_proba = pref_proba * cur_step_prob[idx] |
| + cur_char = char |
| + |
| + if char == empty_tok: |
| + cur_pref = pref |
| + else: |
| + if prev_char != char: |
| + cur_pref = pref + char |
| + else: |
| + cur_pref = pref |
| + |
| + new_dp[(cur_pref, cur_char)] += cur_proba |
| + |
| + return new_dp |
| + |
| + |
| +def truncate_beams(dp, beam_size): |
| + items = sorted(list(dp.items()), key=lambda x: -x[1])[:beam_size] |
| + return dict(items) |
| |
| |
| |
| |
| @@ -1,29 +1,17 @@ |
| -from typing import List |
| +from src.metrics.base_metric import ArgmaxMetric, BeamSearchMetric, CustomBeamSearchMetric |
| +from src.metrics.utils import calc_wer |
| |
| -import torch |
| -from torch import Tensor |
| |
| -from src.metrics.base_metric import BaseMetric |
| -from src.metrics.utils import calc_wer |
| +class ArgmaxWERMetric(ArgmaxMetric): |
| + def __init__(self, *args, **kwargs): |
| + super().__init__(*args, binary_func=calc_wer, **kwargs) |
| |
| -# TODO beam search / LM versions |
| -# Note: they can be written in a pretty way |
| -# Note 2: overall metric design can be significantly improved |
| |
| +class BeamSearchWERMetric(BeamSearchMetric): |
| + def __init__(self, *args, **kwargs): |
| + super().__init__(*args, binary_func=calc_wer, **kwargs) |
| |
| -class ArgmaxWERMetric(BaseMetric): |
| - def __init__(self, text_encoder, *args, **kwargs): |
| - super().__init__(*args, **kwargs) |
| - self.text_encoder = text_encoder |
| |
| - def __call__( |
| - self, log_probs: Tensor, log_probs_length: Tensor, text: List[str], **kwargs |
| - ): |
| - wers = [] |
| - predictions = torch.argmax(log_probs.cpu(), dim=-1).numpy() |
| - lengths = log_probs_length.detach().numpy() |
| - for log_prob_vec, length, target_text in zip(predictions, lengths, text): |
| - target_text = self.text_encoder.normalize_text(target_text) |
| - pred_text = self.text_encoder.ctc_decode(log_prob_vec[:length]) |
| - wers.append(calc_wer(target_text, pred_text)) |
| - return sum(wers) / len(wers) |
| +class CustomBeamSearchWERMetric(CustomBeamSearchMetric): |
| + def __init__(self, *args, **kwargs): |
| + super().__init__(*args, binary_func=calc_wer, **kwargs) |
| |
| |
| |
| |
| @@ -1,5 +1,7 @@ |
| from src.model.baseline_model import BaselineModel |
| +from src.model.conformer_model import ConformerModel |
| |
| __all__ = [ |
| "BaselineModel", |
| + "ConformerModel", |
| ] |
| |
| |
| |
| |
| @@ -1 +1,2 @@ |
| from src.text_encoder.ctc_text_encoder import CTCTextEncoder |
| +from src.text_encoder.sp_ctc_text_encoder import SPCTCTextEncoder |
| |
| |
| |
| |
| @@ -3,12 +3,6 @@ from string import ascii_lowercase |
| |
| import torch |
| |
| -# TODO add CTC decode |
| -# TODO add BPE, LM, Beam Search support |
| -# Note: think about metrics and encoder |
| -# The design can be remarkably improved |
| -# to calculate stuff more efficiently and prettier |
| - |
| |
| class CTCTextEncoder: |
| EMPTY_TOK = "" |
| @@ -59,7 +53,23 @@ class CTCTextEncoder: |
| return "".join([self.ind2char[int(ind)] for ind in inds]).strip() |
| |
| def ctc_decode(self, inds) -> str: |
| - pass # TODO |
| + seq = inds.tolist() |
| + |
| + blank_id = self.char2ind[self.EMPTY_TOK] |
| + result_chars = "" |
| + prev = None |
| + |
| + for idx in seq: |
| + if idx == blank_id: |
| + prev = idx |
| + continue |
| + if idx == prev: |
| + continue |
| + ch = self.ind2char[idx] |
| + result_chars += ch |
| + prev = idx |
| + |
| + return result_chars.strip() |
| |
| @staticmethod |
| def normalize_text(text: str): |
| |
| |
| |
| |
| @@ -136,26 +136,24 @@ class Inferencer(BaseTrainer): |
| # Some saving logic. This is an example |
| # Use if you need to save predictions on disk |
| |
| - batch_size = batch["logits"].shape[0] |
| - current_id = batch_idx * batch_size |
| - |
| - for i in range(batch_size): |
| - # clone because of |
| - # https://github.com/pytorch/pytorch/issues/1995 |
| - logits = batch["logits"][i].clone() |
| - label = batch["labels"][i].clone() |
| - pred_label = logits.argmax(dim=-1) |
| - |
| - output_id = current_id + i |
| - |
| - output = { |
| - "pred_label": pred_label, |
| - "label": label, |
| - } |
| - |
| - if self.save_path is not None: |
| - # you can use safetensors or other lib here |
| - torch.save(output, self.save_path / part / f"output_{output_id}.pth") |
| + # batch_size = batch["log_probs"].shape[0] |
| + # current_id = batch_idx * batch_size |
| + |
| + # for i in range(batch_size): |
| + # logits = batch["log_probs"][i].clone() |
| + # label = batch["labels"][i].clone() |
| + # pred_label = logits.argmax(dim=-1) |
| + |
| + # output_id = current_id + i |
| + |
| + # output = { |
| + # "pred_label": pred_label, |
| + # "label": label, |
| + # } |
| + |
| + # if self.save_path is not None: |
| + # # you can use safetensors or other lib here |
| + # torch.save(output, self.save_path / part / f"output_{output_id}.pth") |
| |
| return batch |
| |
| |
| |
| |
| |
| @@ -86,7 +86,7 @@ class Trainer(BaseTrainer): |
| |
| def log_spectrogram(self, spectrogram, **batch): |
| spectrogram_for_plot = spectrogram[0].detach().cpu() |
| - image = plot_spectrogram(spectrogram_for_plot) |
| + image = plot_spectrogram(spectrogram_for_plot).permute(1, 2, 0) |
| self.writer.add_image("spectrogram", image) |
| |
| def log_predictions( |
| |
| |
| |
| |
| @@ -57,12 +57,20 @@ def main(config): |
| |
| # build optimizer, learning rate scheduler |
| trainable_params = filter(lambda p: p.requires_grad, model.parameters()) |
| + |
| optimizer = instantiate(config.optimizer, params=trainable_params) |
| - lr_scheduler = instantiate(config.lr_scheduler, optimizer=optimizer) |
| |
| # epoch_len = number of iterations for iteration-based training |
| # epoch_len = None or len(dataloader) for epoch-based training |
| - epoch_len = config.trainer.get("epoch_len") |
| + epoch_len = config.trainer.get("epoch_len") or len(dataloaders["train"]) |
| + |
| + sched_kwargs = { |
| + "optimizer": optimizer, |
| + } |
| + if "steps_per_epoch" in config.lr_scheduler: |
| + sched_kwargs["steps_per_epoch"] = epoch_len |
| + |
| + lr_scheduler = instantiate(config.lr_scheduler, **sched_kwargs) |
| |
| trainer = Trainer( |
| model=model, |
|
|