| from pathlib import Path |
| import re |
| import os |
| import itertools |
|
|
| from beartype import beartype |
|
|
| import torch |
| from torch import nn |
| from torch.optim.lr_scheduler import CosineAnnealingLR |
|
|
| from .dataset import get_dataloader, audioDataset |
| from .optimizer import get_optimizer |
| from torch.utils import tensorboard |
| from .loss import * |
| import json |
| from speechtokenizer import SpeechTokenizer |
| import time |
| from tqdm import tqdm |
| from accelerate import Accelerator, DistributedType, DistributedDataParallelKwargs, DataLoaderConfiguration |
|
|
|
|
| |
|
|
| def exists(val): |
| return val is not None |
|
|
| def cycle(dl): |
| while True: |
| for data in dl: |
| yield data |
|
|
| def cast_tuple(t): |
| return t if isinstance(t, (tuple, list)) else (t,) |
|
|
|
|
| def accum_log(log, new_logs): |
| for key, new_value in new_logs.items(): |
| old_value = log.get(key, 0.) |
| log[key] = old_value + new_value |
| return log |
|
|
| def checkpoint_num_steps(checkpoint_path): |
| """Returns the number of steps trained from a checkpoint based on the filename. |
| |
| Filename format assumed to be something like "/path/to/soundstorm.20000.pt" which is |
| for 20k train steps. Returns 20000 in that case. |
| """ |
| results = re.findall(r'\d+', str(checkpoint_path)) |
|
|
| if len(results) == 0: |
| return 0 |
|
|
| return int(results[-1]) |
|
|
|
|
| class SpeechTokenizerTrainer(nn.Module): |
| @beartype |
| def __init__( |
| self, |
| generator: SpeechTokenizer, |
| discriminators: dict, |
| cfg, |
| accelerate_kwargs: dict = dict(), |
| ): |
| super().__init__() |
| ddp_kwargs = DistributedDataParallelKwargs() |
| torch.manual_seed(cfg.get('seed')) |
| split_batches = cfg.get("split_batches", False) |
| self.log_steps = cfg.get('log_steps') |
| self.stdout_steps = cfg.get('stdout_steps') |
| self.save_model_steps = cfg.get('save_model_steps') |
| results_folder = cfg.get('results_folder') |
| self.results_folder = Path(results_folder) |
| self.num_ckpt_keep = cfg.get("num_ckpt_keep") |
| self.epochs = cfg.get("epochs") |
| self.num_warmup_steps = cfg.get("num_warmup_steps") |
| self.batch_size = cfg.get("batch_size") |
| self.sample_rate = cfg.get('sample_rate') |
| self.showpiece_num = cfg.get('showpiece_num', 8) |
| project_name = 'SpeechTokenizer' |
| |
| if not self.results_folder.exists(): |
| self.results_folder.mkdir(parents = True, exist_ok = True) |
| |
| with open(f'{str(self.results_folder)}/config.json', 'w+') as f: |
| json.dump(cfg, f, ensure_ascii=False, indent=4) |
| |
| |
| |
| dataloader_config = DataLoaderConfiguration(split_batches=split_batches) |
| self.accelerator = Accelerator( |
| dataloader_config=dataloader_config, |
| kwargs_handlers=[ddp_kwargs], |
| |
| **accelerate_kwargs |
| ) |
| |
| if self.is_main: |
| self.writer = tensorboard.SummaryWriter(os.path.join(results_folder, 'logs')) |
|
|
| self.generator = generator |
| self.discriminators = discriminators |
| |
|
|
| self.register_buffer('steps', torch.Tensor([0])) |
|
|
| |
| |
| self.mel_loss_lambdas = cfg.get('mel_loss_lambdas') |
| self.commitment_loss_lambda = cfg.get('commitment_loss_lambda') |
| self.recon_loss_lambda = cfg.get('recon_loss_lambda') |
| self.distill_loss_lambda = cfg.get('distill_loss_lambda') |
| distill_type = cfg.get('distill_type', 'd_axis') |
| if distill_type == 't_axis': |
| from functools import partial |
| lambda_sim = cfg.get('lambda_sim', 1) |
| self.distill_loss = partial(t_axis_distill_loss, lambda_sim=lambda_sim) |
| else: |
| self.distill_loss = d_axis_distill_loss |
| self.mel_loss_kwargs_list = [] |
| mult = 1 |
| for i in range(len(self.mel_loss_lambdas)): |
| self.mel_loss_kwargs_list.append({'n_fft': cfg.get('n_fft') // mult, 'num_mels':cfg.get('num_mels'),'sample_rate':self.sample_rate, |
| 'hop_size': cfg.get('hop_size') // mult, 'win_size':cfg.get('win_size') // mult, 'fmin':cfg.get('fmin'), |
| 'fmax':cfg.get('fmax_for_loss')}) |
| mult = mult * 2 |
| self.mel_kwargs = {'n_fft': cfg.get('n_fft'), 'num_mels':cfg.get('num_mels'),'sample_rate':self.sample_rate, |
| 'hop_size': cfg.get('hop_size'), 'win_size':cfg.get('win_size'), 'fmin':cfg.get('fmin'), |
| 'fmax':cfg.get('fmax')} |
| |
|
|
| |
|
|
| |
| segment_size = cfg.get("segment_size") |
| train_files = cfg.get("train_files") |
| batch_size = cfg.get("batch_size") |
| self.batch_size = batch_size |
| with open(train_files, 'r') as f: |
| train_file_list = f.readlines() |
| valid_files = cfg.get("valid_files") |
| with open(valid_files, 'r') as f: |
| valid_file_list = f.readlines() |
| |
| self.ds = audioDataset(file_list=train_file_list, |
| segment_size=segment_size, |
| downsample_rate=generator.downsample_rate, |
| sample_rate=self.sample_rate) |
| self.valid_ds = audioDataset(file_list=valid_file_list, |
| segment_size=self.sample_rate * 30, |
| downsample_rate=generator.downsample_rate, |
| sample_rate=self.sample_rate, |
| valid=True) |
| if self.is_main: |
| self.print(f'training with dataset of {len(self.ds)} samples and validating with randomly splitted {len(self.valid_ds)} samples') |
| |
|
|
|
|
| assert len(self.ds) >= self.batch_size, 'dataset must have sufficient samples for training' |
| assert len(self.valid_ds) >= self.batch_size, f'validation dataset must have sufficient number of samples (currently {len(self.valid_ds)}) for training' |
|
|
| |
| drop_last = cfg.get("drop_last", True) |
| num_workers = cfg.get("num_workers") |
| self.dl = get_dataloader(self.ds, batch_size = self.batch_size, shuffle = True, drop_last = drop_last, num_workers=num_workers) |
| self.valid_dl = get_dataloader(self.valid_ds, batch_size = 1, shuffle = False, drop_last = False, num_workers=1) |
| |
| |
| self.lr = cfg.get("learning_rate") |
| self.initial_lr = cfg.get("intial_learning_rate") |
| |
| |
| self.optim_g = get_optimizer( |
| generator.parameters(), |
| lr = cfg.get("learning_rate"), |
| wd = cfg.get("wd"), |
| betas = cfg.get("betas") |
| ) |
| |
| self.optim_d = get_optimizer( |
| itertools.chain(*[i.parameters() for i in self.discriminators.values()]), |
| lr = cfg.get("learning_rate"), |
| wd = cfg.get("wd"), |
| betas = cfg.get("betas") |
| ) |
|
|
| |
| |
| num_train_steps = self.epochs * self.ds.__len__() // batch_size |
| self.scheduler_g = CosineAnnealingLR(self.optim_g, T_max = num_train_steps) |
| self.scheduler_d = CosineAnnealingLR(self.optim_d, T_max = num_train_steps) |
| |
| |
|
|
| |
| |
|
|
| ( |
| self.generator, |
| self.optim_g, |
| self.optim_d, |
| self.scheduler_g, |
| self.scheduler_d, |
| self.dl, |
| self.valid_dl |
| ) = self.accelerator.prepare( |
| self.generator, |
| self.optim_g, |
| self.optim_d, |
| self.scheduler_g, |
| self.scheduler_d, |
| self.dl, |
| self.valid_dl |
| ) |
| self.discriminators = {k:self.accelerator.prepare(v) for k, v in self.discriminators.items()} |
| |
| |
| |
| hps = {"num_train_steps": num_train_steps, "num_warmup_steps": self.num_warmup_steps, "learning_rate": self.lr, "initial_learning_rate": self.initial_lr, "epochs": self.epochs} |
| self.accelerator.init_trackers("SpeechTokenizer", config=hps) |
| self.best_dev_mel_loss = float('inf') |
| self.plot_gt_once = False |
|
|
| def save(self, path, best_dev_mel_loss): |
| if best_dev_mel_loss < self.best_dev_mel_loss: |
| self.best_dev_mel_loss = best_dev_mel_loss |
| torch.save(self.accelerator.get_state_dict(self.generator), f'{self.results_folder}/SpeechTokenizer_best_dev.pt') |
| ckpts = sorted(Path(path).parent.glob(f'SpeechTokenizerTrainer_*')) |
| if len(ckpts) > self.num_ckpt_keep: |
| [os.remove(c) for c in ckpts[:-self.num_ckpt_keep]] |
| pkg = dict( |
| generator = self.accelerator.get_state_dict(self.generator), |
| discriminators = {k:self.accelerator.get_state_dict(v) for k, v in self.discriminators.items()}, |
| optim_g = self.optim_g.state_dict(), |
| optim_d = self.optim_d.state_dict(), |
| scheduler_g = self.scheduler_g.state_dict(), |
| scheduler_d = self.scheduler_d.state_dict(), |
| best_dev_mel_loss = self.best_dev_mel_loss |
| ) |
| torch.save(pkg, path) |
|
|
| def load(self, path = None, restore_optimizer = True): |
| if not exists(path): |
| ckpts = sorted(self.results_folder.glob(f'SpeechTokenizerTrainer_*')) |
| path = str(ckpts[-1]) |
| generator = self.accelerator.unwrap_model(self.generator) |
| pkg = torch.load(path, map_location='cpu') |
| generator.load_state_dict(pkg['generator']) |
| discriminators = {k:self.accelerator.unwrap_model(v) for k, v in self.discriminators.items()} |
| map(lambda kv: kv[1].load_state_dict(pkg['discriminators'][kv[0]]), discriminators.items()) |
|
|
| if restore_optimizer: |
| self.optim_d.load_state_dict(pkg['optim_d']) |
| self.scheduler_d.load_state_dict(pkg['scheduler_d']) |
| self.optim_g.load_state_dict(pkg['optim_g']) |
| self.scheduler_g.load_state_dict(pkg['scheduler_g']) |
| if 'best_dev_mel_loss' in pkg.keys(): |
| self.best_dev_mel_loss = pkg['best_dev_mel_loss'] |
| if self.is_main: |
| self.print(f'The best dev mel loss before is {self.best_dev_mel_loss}') |
|
|
| |
| self.steps = torch.tensor([checkpoint_num_steps(path) + 1], device=self.device) |
|
|
| def print(self, msg): |
| self.accelerator.print(msg) |
|
|
| @property |
| def device(self): |
| return self.accelerator.device |
|
|
| @property |
| def is_distributed(self): |
| return not (self.accelerator.distributed_type == DistributedType.NO and self.accelerator.num_processes == 1) |
|
|
| @property |
| def is_main(self): |
| return self.accelerator.is_main_process |
|
|
| @property |
| def is_local_main(self): |
| return self.accelerator.is_local_main_process |
|
|
| def warmup(self, step): |
| if step < self.num_warmup_steps: |
| return self.initial_lr + (self.lr - self.initial_lr) * step / self.num_warmup_steps |
| else: |
| return self.lr |
| |
| def log(self, values: dict, step, type=None, **kwargs): |
| if type == 'figure': |
| for k, v in values.items(): |
| self.writer.add_figure(k, v, global_step=step) |
| elif type == 'audio': |
| for k, v in values.items(): |
| self.writer.add_audio(k, v, global_step=step, **kwargs) |
| else: |
| for k, v in values.items(): |
| self.writer.add_scalar(k, v, global_step=step) |
|
|
| def train(self): |
| |
| self.generator.train() |
| map(lambda disc:disc.train(), self.discriminators.values()) |
| step_time_log = {} |
| |
| steps = int(self.steps.item()) |
| if steps < self.num_warmup_steps: |
| lr = self.warmup(steps) |
| for param_group in self.optim.param_groups: |
| param_group['lr'] = lr |
| else: |
| self.scheduler_d.step() |
| self.scheduler_g.step() |
| lr = self.scheduler_d.get_last_lr()[0] |
| |
| for epoch in range(self.epochs): |
| if self.is_main: |
| print(f'Epoch:{epoch} start...') |
| |
| for batch in self.dl: |
| |
| tic = time.time() |
| |
| x, semantic_feature = batch |
| x = x.unsqueeze(1) |
| x_hat, loss_q, feature = self.generator(x) |
| |
| |
| self.optim_d.zero_grad() |
| discriminator_outputs = list(map(lambda disc:disc(x, x_hat.detach()), self.discriminators.values())) |
| loss_disc_all = sum(map(lambda x:discriminator_loss(*x[:2]), discriminator_outputs)) |
| |
| self.accelerator.backward(loss_disc_all) |
| self.optim_d.step() |
| |
| |
| self.optim_g.zero_grad() |
| discriminator_outputs = list(map(lambda disc:disc(x, x_hat), self.discriminators.values())) |
| loss_recon = recon_loss(x, x_hat) |
| loss_mel = sum(map(lambda mel_k:mel_k[0] * mel_loss(x, x_hat, **mel_k[1]), zip(self.mel_loss_lambdas, self.mel_loss_kwargs_list))) |
| loss_feature = sum(map(lambda x:feature_loss(*x[2:]), discriminator_outputs)) |
| loss_adversarial = sum(map(lambda x:adversarial_loss(x[1]), discriminator_outputs)) |
| loss_distill = self.distill_loss(feature, semantic_feature) |
| loss_generator_all = loss_feature + loss_adversarial + loss_mel + loss_q * self.commitment_loss_lambda + loss_recon * self.recon_loss_lambda + self.distill_loss_lambda * loss_distill |
| self.accelerator.backward(loss_generator_all) |
| |
| |
| self.optim_g.step() |
| |
| step_time_log = accum_log(step_time_log, {'time_cost': time.time() - tic}) |
| |
| |
| |
| |
| if self.is_main and not (steps % self.stdout_steps): |
| with torch.inference_mode(): |
| mel_error = mel_loss(x, x_hat, **self.mel_loss_kwargs_list[0]).item() |
| self.print(f"Epoch {epoch} -- Step {steps}: Gen Loss: {loss_generator_all.item():0.3f}; Mel Error:{mel_error:0.3f}; Q Loss: {loss_q.item():0.3f}; Distill Loss: {loss_distill.item():0.3f}; Time cost per step: {step_time_log['time_cost'] / self.stdout_steps:0.3f}s") |
| step_time_log = {} |
| if self.is_main and not (steps % self.log_steps): |
| self.log({"train/discriminators loss": loss_disc_all.item(), "train/generator loss": loss_generator_all.item(), "train/feature loss": loss_feature.item(), |
| "train/adversarial loss": loss_adversarial.item(), "train/quantizer loss": loss_q.item(), "train/mel loss": loss_mel.item(), |
| "train/mel error": mel_error, "train/distillation loss": loss_distill.item(), "train/learning_rate": lr}, step=steps) |
| |
| self.accelerator.wait_for_everyone() |
| |
| |
| if self.is_main and not(steps % self.save_model_steps) and steps != 0: |
| |
| self.print('Validation start ...') |
| |
| total_mel_error = 0.0 |
| total_distill_loss = 0.0 |
| num = 0 |
| self.generator.eval() |
| with torch.inference_mode(): |
| for i, batch in tqdm(enumerate(self.valid_dl)): |
| x, semantic_feature = batch |
| x = x.unsqueeze(1) |
| x_hat, loss_q, feature = self.generator(x) |
| mel_error = mel_loss(x, x_hat, **self.mel_loss_kwargs_list[0]).item() |
| total_mel_error += mel_error |
| loss_distill = self.distill_loss(feature, semantic_feature).item() |
| total_distill_loss += loss_distill |
| num += x.size(0) |
| if i < self.showpiece_num: |
| if not self.plot_gt_once: |
| self.log({f'groundtruth/x_{i}': x[0].cpu().detach()}, type='audio', sample_rate=self.sample_rate, step=steps) |
| x_spec = mel_spectrogram(x.squeeze(1), **self.mel_kwargs) |
| self.log({f'groundtruth/x_spec_{i}': plot_spectrogram(x_spec[0].cpu().numpy())}, type='figure', step=steps) |
| |
| self.log({f'generate/x_hat_{i}': x_hat[0].cpu().detach()}, type='audio', sample_rate=self.sample_rate, step=steps) |
| x_hat_spec = mel_spectrogram(x_hat.squeeze(1), **self.mel_kwargs) |
| self.log({f'generate/x_hat_spec_{i}': plot_spectrogram(x_hat_spec[0].cpu().numpy())}, type='figure', step=steps) |
| if not self.plot_gt_once: |
| self.plot_gt_once = True |
| self.print(f'{steps}: dev mel error: {total_mel_error / num:0.3f}\tdev distill loss: {total_distill_loss / num:0.3f}') |
| self.log({'dev/mel error': total_mel_error / num, 'dev/distillation loss': total_distill_loss / num}, step=steps) |
| |
| |
| |
| model_path = str(self.results_folder / f'SpeechTokenizerTrainer_{steps:08d}') |
| self.save(model_path, total_mel_error / num) |
| self.print(f'{steps}: saving model to {str(self.results_folder)}') |
| self.generator.train() |
| |
| |
| self.steps += 1 |
| steps = int(self.steps.item()) |
| if steps < self.num_warmup_steps: |
| lr = self.warmup(steps) |
| for param_group in self.optim_g.param_groups: |
| param_group['lr'] = lr |
| for param_group in self.optim_d.param_groups: |
| param_group['lr'] = lr |
| else: |
| self.scheduler_d.step() |
| self.scheduler_g.step() |
| lr = self.scheduler_g.get_last_lr()[0] |
| |
| self.print('training complete') |
| |
| def continue_train(self): |
| self.load() |
| self.train() |
| |
| |