Source code for compiam.structure.segmentation.dhrupad_bandish_segmentation

import os
import glob
import librosa

import numpy as np
import matplotlib.pyplot as plt
from compiam.exceptions import ModelNotTrainedError

from compiam.data import WORKDIR
from compiam.utils import get_logger
from compiam.utils.download import download_remote_model

logger = get_logger(__name__)


[docs] class DhrupadBandishSegmentation: """Dhrupad Bandish Segmentation""" def __init__( self, mode="net", fold=0, model_path=None, splits_path=None, annotations_path=None, features_path=None, original_audios_path=None, processed_audios_path=None, download_link=None, download_checksum=None, device=None, ): """Dhrupad Bandish Segmentation init method. :param mode: net, voc, or pakh. That indicates the source for s.t.m. estimation. Use the net mode if audio is a mixture signal, else use voc or pakh for clean/source-separated vocals or pakhawaj tracks. :param fold: 0, 1 or 2, it is the validation fold to use during training. :param model_path: path to file to the model weights. :param splits_path: path to file to audio splits. :param annotations_path: path to file to the annotations. :param features_path: path to file to the computed features. :param original_audios_path: path to file to the original audios from the dataset (see README.md in compIAM/models/structure/dhrupad_bandish_segmentation/audio_original) :param processed_audios_path: path to file to the processed audio files. :param download_link: link to the remote pre-trained model. :param download_checksum: checksum of the model file. :param device: indicate whether the model will run on the GPU. """ ### IMPORTING OPTIONAL DEPENDENCIES try: global torch import torch global split_audios from compiam.structure.segmentation.dhrupad_bandish_segmentation.audio_processing import ( split_audios, ) global extract_features, makechunks from compiam.structure.segmentation.dhrupad_bandish_segmentation.feature_extraction import ( extract_features, makechunks, ) global class_to_categorical, categorical_to_class, build_model, smooth_boundaries from compiam.structure.segmentation.dhrupad_bandish_segmentation.model_utils import ( class_to_categorical, categorical_to_class, build_model, smooth_boundaries, ) global pars import compiam.structure.segmentation.dhrupad_bandish_segmentation.params as pars except: raise ImportError( "In order to use this tool you need to have torch installed. " "Install compIAM with torch support: pip install 'compiam[torch]'" ) ### if not device: self.device = "cuda" if torch.cuda.is_available() else "cpu" # Load mode by default: update with self.update_mode() self.mode = mode # Load fold by default: update with self.update_fold() self.fold = fold self.classes = pars.classes_dict[self.mode] # To prevent CUDNN_STATUS_NOT_INITIALIZED error in case of incompatible GPU try: self.model = self._build_model() except: self.device = "cpu" self.model = self._build_model() self.model_path = model_path self.download_link = download_link self.download_checksum = download_checksum self.loaded_model_path = None self.trained = False if self.model_path is not None: path_to_model = os.path.join( self.model_path[self.mode], "saved_model_fold_" + str(self.fold) + ".pt" ) self.load_model(path_to_model) # Loading pre-trained model for given mode self.splits_path = ( splits_path if splits_path is not None else os.path.join( WORKDIR, "models", "structure", "dhrupad_bandish_segmentation", "splits" ) ) self.annotations_path = ( annotations_path if annotations_path is not None else os.path.join( WORKDIR, "models", "structure", "dhrupad_bandish_segmentation", "annotations", ) ) self.features_path = ( features_path if features_path is not None else os.path.join( WORKDIR, "models", "structure", "dhrupad_bandish_segmentation", "features", ) ) self.original_audios_path = ( original_audios_path if original_audios_path is not None else os.path.join( WORKDIR, "models", "structure", "dhrupad_bandish_segmentation", "audio_original", ) ) self.processed_audios_path = ( processed_audios_path if processed_audios_path is not None else os.path.join( WORKDIR, "models", "structure", "dhrupad_bandish_segmentation", "audio_sections", ) ) def _build_model(self): """Building non-trained model""" return ( build_model(pars.input_height, pars.input_len, len(self.classes)) .float() .to(self.device) )
[docs] def load_model(self, model_path): """Loading weights for model, given self.mode and self.fold :param model_path: path to model weights """ if not os.path.exists(model_path): self.download_model(model_path) self.model = self._build_model() ## Ensuring we can load the model for different torch versions ## -- (weights only might be deprecated) try: self.model.load_state_dict( torch.load(model_path, weights_only=True, map_location=self.device) ) except: self.model.load_state_dict( torch.load(model_path, map_location=self.device) ) self.model.eval() self.loaded_model_path = model_path self.trained = True
[docs] def download_model(self, model_path=None, force_overwrite=False): """Download pre-trained model.""" download_path = ( os.sep + os.path.join(*model_path.split(os.sep)[:-4]) if model_path is not None else os.path.join( WORKDIR, "models", "structure", "dhrupad_bandish_segmentation" ) ) # Creating model folder to store the weights if not os.path.exists(download_path): os.makedirs(download_path) download_remote_model( self.download_link, self.download_checksum, download_path, force_overwrite=force_overwrite, )
[docs] def update_mode(self, mode): """Update mode for the training and sampling. Mode is one of net, voc, pakh, indicating the source for s.t.m. estimation. Use the net mode if audio is a mixture signal, else use voc or pakh for clean/source-separated vocals or pakhawaj tracks. :param mode: new mode to use """ self.mode = mode self.classes = pars.classes_dict[mode] path_to_model = os.path.join( self.model_path[self.mode], "saved_model_fold_" + str(self.fold) + ".pt" ) self.load_model(path_to_model)
[docs] def update_fold(self, fold): """Update data fold for the training and sampling :param fold: new fold to use """ self.fold = fold path_to_model = os.path.join( self.model_path[self.mode], "saved_model_fold_" + str(self.fold) + ".pt" ) self.load_model(path_to_model)
[docs] def train(self, verbose=True): """Train the Dhrupad Bandish Segmentation model :param verbose: showing details of the model """ logger.info("Splitting audios...") split_audios( save_dir=self.processed_audios_path, annotations_path=self.annotations_path, audios_path=self.original_audios_path, ) logger.info("Extracting features...") extract_features( self.processed_audios_path, self.annotations_path, self.features_path, self.mode, ) # generate cross-validation folds for training songlist = os.listdir(self.features_path) labels_stm = np.load( os.path.join(self.features_path, "labels_stm.npy"), allow_pickle=True ).item() partition = {"train": [], "validation": []} n_folds = 3 all_folds = [] for i_fold in range(n_folds): all_folds.append( np.loadtxt( os.path.join( self.splits_path, self.mode, "fold_" + i_fold + ".csv" ), delimiter=",", dtype=str, ) ) val_fold = all_folds[self.fold] train_fold = np.array([]) for i_fold in np.delete(np.arange(0, n_folds), self.fold): if len(train_fold) == 0: train_fold = all_folds[i_fold] else: train_fold = np.append(train_fold, all_folds[i_fold]) for song in songlist: try: ids = glob.glob(os.path.join(self.features_path + song, "*.pt")) except: continue section_name = "_".join(song.split("_")[0:4]) if section_name in val_fold: partition["validation"].extend(ids) elif section_name in train_fold: partition["train"].extend(ids) # generators training_set = torch.utils.data.Dataset( self.features_path, partition["train"], labels_stm ) training_generator = torch.utils.data.data.DataLoader(training_set, **pars) validation_set = torch.utils.data.Dataset( self.features_path, partition["validation"], labels_stm ) validation_generator = torch.utils.data.DataLoader(validation_set, **pars) # model definition and training criterion = torch.nn.CrossEntropyLoss(reduction="mean") optimizer = torch.optim.Adam(self.parameters(), lr=0.0001) if verbose: logger.info(self.model) n_params = 0 for param in self.model.parameters(): n_params += torch.prod(torch.tensor(param.shape)) logger.info("Num of trainable params: %d\n" % n_params) ##training epochs loop train_loss_epoch = [] train_acc_epoch = [] val_loss_epoch = [] val_acc_epoch = [] n_idle = 0 if not os.path.exists(os.path.join(self.model_path, self.mode)): os.mkdir(os.path.join(self.model_path, self.mode)) for epoch in range(pars.max_epochs): if n_idle == 50: break train_loss_epoch += [0] train_acc_epoch += [0] val_loss_epoch += [0] val_acc_epoch += [0] n_iter = 0 ##training self.model.train() for local_batch, local_labels, _ in training_generator: # map labels to class ids local_labels = class_to_categorical(local_labels, self.classes) # add channel dimension if len(local_batch.shape) == 3: local_batch = local_batch.unsqueeze(1) # transfer to GPU local_batch, local_labels = local_batch.float().to( self.device ), local_labels.to(self.device) # update weights optimizer.zero_grad() outs = self.model(local_batch).squeeze() loss = criterion(outs, local_labels.long()) loss.backward() optimizer.step() # append loss and acc to arrays train_loss_epoch[-1] += loss.item() acc = ( np.sum( ( np.argmax(outs.detach().cpu().numpy(), 1) == local_labels.detach().cpu().numpy() ) ) / pars.batch_size ) train_acc_epoch[-1] += acc n_iter += 1 train_loss_epoch[-1] /= n_iter train_acc_epoch[-1] /= n_iter n_iter = 0 ##validation self.model.eval() with torch.set_grad_enabled(False): for local_batch, local_labels, _ in validation_generator: # map labels to class ids local_labels = pars.class_to_categorical(local_labels, self.classes) # add channel dimension if len(local_batch.shape) == 3: local_batch = local_batch.unsqueeze(1) # transfer to GPU local_batch, local_labels = local_batch.float().to( self.device ), local_labels.to(self.device) # evaluate model outs = self.model(local_batch).squeeze() loss = criterion(outs, local_labels.long()) # append loss and acc to arrays val_loss_epoch[-1] += loss.item() acc = ( np.sum( ( np.argmax(outs.detach().cpu().numpy(), 1) == local_labels.detach().cpu().numpy() ) ) / pars.batch_size ) val_acc_epoch[-1] += acc n_iter += 1 val_loss_epoch[-1] /= n_iter val_acc_epoch[-1] /= n_iter # save if val_loss reduced if val_loss_epoch[-1] == min(val_loss_epoch): torch.save( self.model.state_dict(), os.path.join( self.model_path, self.mode, "saved_model_fold_%d.pt" % self.fold ), ) n_idle = 0 else: n_idle += 1 # log loss in current epoch logger.info( "Epoch no: %d/%d\tTrain loss: %f\tTrain acc: %f\tVal loss: %f\tVal acc: %f" % ( epoch, pars.max_epochs, train_loss_epoch[-1], train_acc_epoch[-1], val_loss_epoch[-1], val_acc_epoch[-1], ) ) self.trained = True
[docs] def predict_stm( self, input_data, input_sr=44100, save_output=False, output_path=None ): """Predict Dhrupad Bandish Segmentation :param input_data: path to audio file or numpy array like audio signal. :param input_sr: sampling rate of the input array of data (if any). This variable is only relevant if the input is an array of data instead of a filepath. :param save_output: boolean indicating whether the output figure for the estimation is stored. :param output_path: if the input is an array, and the user wants to save the estimation, the output_path must be provided, path/to/picture.png. """ if not isinstance(save_output, bool): raise ValueError("save_output must be a boolean") if isinstance(input_data, str): if not os.path.exists(input_data): raise FileNotFoundError("Target audio not found.") audio, sr = librosa.load(input_data, sr=pars.fs) if output_path is None: output_path = os.path.basename(input_data).replace( input_data.split(".")[-1], "png" ) elif isinstance(input_data, np.ndarray): logger.warning( f"Resampling... (input sampling rate is {input_sr}Hz, make sure this is correct)" ) audio = librosa.resample(input_data, orig_sr=input_sr, target_sr=pars.fs) if (save_output is True) and (output_path is None): raise ValueError( "Please provide an output_path in order to save the estimation" ) else: raise ValueError("Input must be path to audio signal or an audio array") if save_output is True: if not os.path.exists(os.path.basename(output_path)): os.mkdir(os.path.basename(output_path)) if self.trained is False: raise ModelNotTrainedError( """ Model is not trained. Please load model before running inference! You can load the pre-trained instance with the load_model wrapper. """ ) # convert to mel-spectrogram melgram = librosa.feature.melspectrogram( y=audio, sr=pars.fs, n_fft=pars.nfft, hop_length=pars.hopsize, win_length=pars.winsize, n_mels=pars.input_height, fmin=20, fmax=8000, ) melgram = 10 * np.log10(1e-10 + melgram) melgram_chunks = makechunks(melgram, pars.input_len, pars.input_hop) # predict s.t.m. versus time stm_vs_time = [] for chunk in melgram_chunks: model_in = ( (torch.tensor(chunk).unsqueeze(0)).unsqueeze(1).float().to(self.device) ) self.model.to(self.device) model_out = self.model.forward(model_in) model_out = torch.nn.Softmax(1)(model_out).detach().numpy() stm_vs_time.append(np.argmax(model_out)) # smooth predictions with a minimum section duration of 5s stm_vs_time = smooth_boundaries(stm_vs_time, pars.min_sec_dur) # plot plt.plot(np.arange(len(stm_vs_time)) * 0.5, stm_vs_time) plt.yticks(np.arange(-1, 6), [""] + ["1", "2", "4", "8", "16"] + [""]) plt.grid("on", linestyle="--", axis="y") plt.xlabel("Time (s)", fontsize=12) plt.ylabel("Surface tempo multiple", fontsize=12) if save_output is True: plt.savefig(output_path) else: plt.show() return stm_vs_time