From d6b68ef90bbb1ef4fdcb80240b7e8963127ea472 Mon Sep 17 00:00:00 2001 From: FelixChan <2223485532@qq,com> Date: Mon, 13 Oct 2025 17:56:36 +0800 Subject: [PATCH] 1013 update --- .gitignore | 1 + Amadeus/custom_x_transformers.py | 5 +- Amadeus/sampling_utils.py | 4 +- Amadeus/symbolic_encoding/data_utils.py | 167 ++++--- Amadeus/symbolic_encoding/midi2audio.py | 2 +- Amadeus/symbolic_yamls/config-accelerate.yaml | 11 +- ...b8_embSum_diff_t2m_150M_pretrainingv2.yaml | 4 +- ...b8_embSum_diff_t2m_600M_finetunningv2.yaml | 19 + ...b8_embSum_diff_t2m_600M_pretrainingv2.yaml | 19 + Amadeus/transformer_utils.py | 103 +++- .../step1_midi2corpus_fined.py | 2 +- generate-batch.py | 1 + len_tunes/FinetuneDataset/len_nb8.png | Bin 0 -> 24166 bytes len_tunes/IrishMan/len_nb8.png | Bin 0 -> 23011 bytes len_tunes/gigamidi/len_nb8.png | Bin 0 -> 24892 bytes midi_stastic.py | 442 ++++++++++++++++++ ,idi_sim.py | 105 +++++ 17 files changed, 815 insertions(+), 70 deletions(-) create mode 100644 Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_finetunningv2.yaml create mode 100644 Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_pretrainingv2.yaml create mode 100644 len_tunes/FinetuneDataset/len_nb8.png create mode 100644 len_tunes/IrishMan/len_nb8.png create mode 100644 len_tunes/gigamidi/len_nb8.png create mode 100644 midi_stastic.py create mode 100644 ,idi_sim.py diff --git a/.gitignore b/.gitignore index 8caba3c..3ae93de 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ wandb/ .vscode/ checkpoints/ metadata/ +*.sf2 diff --git a/Amadeus/custom_x_transformers.py b/Amadeus/custom_x_transformers.py index 64759b0..fbfb538 100644 --- a/Amadeus/custom_x_transformers.py +++ b/Amadeus/custom_x_transformers.py @@ -1358,6 +1358,7 @@ class Attention(Module): dim_latent_kv = None, latent_rope_subheads = None, onnxable = False, + use_gated_attention = False, # https://arxiv.org/abs/2505.06708 attend_sdp_kwargs: dict = dict( enable_flash = True, enable_math = True, @@ -1387,6 +1388,7 @@ class Attention(Module): k_dim = dim_head * kv_heads v_dim = value_dim_head * kv_heads out_dim = value_dim_head * heads + gated_dim = out_dim # determine input dimensions to qkv based on whether intermediate latent q and kv are being used # for eventually supporting multi-latent attention (MLA) @@ -1447,7 +1449,8 @@ class Attention(Module): self.to_v_gate = None if gate_values: - self.to_v_gate = nn.Linear(dim, out_dim) + # self.to_v_gate = nn.Linear(dim, out_dim) + self.to_v_gate = nn.Linear(dim_kv_input, gated_dim) self.to_v_gate_activation = F.silu if swiglu_values else F.sigmoid nn.init.constant_(self.to_v_gate.weight, 0) nn.init.constant_(self.to_v_gate.bias, 10) diff --git a/Amadeus/sampling_utils.py b/Amadeus/sampling_utils.py index 28f652b..c5742ca 100644 --- a/Amadeus/sampling_utils.py +++ b/Amadeus/sampling_utils.py @@ -1,6 +1,6 @@ import torch import torch.nn.functional as F - + def top_p_sampling(logits, thres=0.9): sorted_logits, sorted_indices = torch.sort(logits, descending=True) cum_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) @@ -84,7 +84,7 @@ def sample_with_prob(logits, sampling_method, threshold, temperature): # temporarily apply the sampling method to logits logits = logits / temperature # logits = add_gumbel_noise(logits, temperature) - + if sampling_method == "top_p": modified_logits = top_p_sampling(logits, thres=threshold) elif sampling_method == "typical": diff --git a/Amadeus/symbolic_encoding/data_utils.py b/Amadeus/symbolic_encoding/data_utils.py index 0d23e84..23e4583 100644 --- a/Amadeus/symbolic_encoding/data_utils.py +++ b/Amadeus/symbolic_encoding/data_utils.py @@ -530,6 +530,62 @@ class Melody(SymbolicMusicDataset): test_names.extend(song_dict[song_name]) return train_names, valid_names, test_names, shuffled_tune_names +class msmidi(SymbolicMusicDataset): + def __init__(self, vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path=None, + for_evaluation: bool = False): + super().__init__(vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path, + for_evaluation=for_evaluation) + + def _load_tune_in_idx(self) -> Tuple[Dict[str, np.ndarray], Dict[str, int], List[str]]: + ''' + Irregular tunes are removed from the dataset for better generation quality + It includes tunes that are not quantized properly, mostly theay are expressive performance data + ''' + print("preprocessed tune_in_idx data is being loaded") + tune_in_idx_list = sorted(list(Path(f"dataset/represented_data/tuneidx/tuneidx_{self.__class__.__name__}/{self.encoding_scheme}{self.num_features}").rglob("*.npz"))) + if self.debug: + tune_in_idx_list = tune_in_idx_list[:5000] + tune_in_idx_dict = OrderedDict() + len_tunes = OrderedDict() + file_name_list = [] + with open("metadata/LakhClean_irregular_tunes.json", "r") as f: + irregular_tunes = json.load(f) + for tune_in_idx_file in tqdm(tune_in_idx_list, total=len(tune_in_idx_list)): + if tune_in_idx_file.stem in irregular_tunes: + continue + tune_in_idx = np.load(tune_in_idx_file)['arr_0'] + tune_in_idx_dict[tune_in_idx_file.stem] = tune_in_idx + len_tunes[tune_in_idx_file.stem] = len(tune_in_idx) + file_name_list.append(tune_in_idx_file.stem) + print(f"number of loaded tunes: {len(tune_in_idx_dict)}") + return tune_in_idx_dict, len_tunes, file_name_list + + def _get_split_list_from_tune_in_idx(self, ratio, seed): + ''' + As Lakh dataset contains multiple versions of the same song, we split the dataset based on the song name + ''' + shuffled_tune_names = list(self.tune_in_idx.keys()) + song_names_without_version = [re.sub(r"\.\d+$", "", song) for song in shuffled_tune_names] + song_dict = {} + for song, orig_song in zip(song_names_without_version, shuffled_tune_names): + if song not in song_dict: + song_dict[song] = [] + song_dict[song].append(orig_song) + unique_song_names = list(song_dict.keys()) + random.seed(seed) + random.shuffle(unique_song_names) + num_train = int(len(unique_song_names)*ratio) + num_valid = int(len(unique_song_names)*(1-ratio)/2) + train_names = [] + valid_names = [] + test_names = [] + for song_name in unique_song_names[:num_train]: + train_names.extend(song_dict[song_name]) + for song_name in unique_song_names[num_train:num_train+num_valid]: + valid_names.extend(song_dict[song_name]) + for song_name in unique_song_names[num_train+num_valid:]: + test_names.extend(song_dict[song_name]) + return train_names, valid_names, test_names, shuffled_tune_names class IrishMan(SymbolicMusicDataset): def __init__(self, vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path=None, @@ -648,62 +704,62 @@ class ariamidi(SymbolicMusicDataset): test_names.extend(song_dict[song_name]) return train_names, valid_names, test_names, shuffled_tune_names -class gigamidi(SymbolicMusicDataset): - def __init__(self, vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path=None, - for_evaluation: bool = False): - super().__init__(vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path, - for_evaluation=for_evaluation) +# class gigamidi(SymbolicMusicDataset): +# def __init__(self, vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path=None, +# for_evaluation: bool = False): +# super().__init__(vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path, +# for_evaluation=for_evaluation) - def _load_tune_in_idx(self) -> Tuple[Dict[str, np.ndarray], Dict[str, int], List[str]]: - ''' - Irregular tunes are removed from the dataset for better generation quality - It includes tunes that are not quantized properly, mostly theay are expressive performance data - ''' - print("preprocessed tune_in_idx data is being loaded") - tune_in_idx_list = sorted(list(Path(f"dataset/represented_data/tuneidx/tuneidx_{self.__class__.__name__}/{self.encoding_scheme}{self.num_features}").rglob("*.npz"))) - if self.debug: - tune_in_idx_list = tune_in_idx_list[:5000] - tune_in_idx_dict = OrderedDict() - len_tunes = OrderedDict() - file_name_list = [] - with open("metadata/LakhClean_irregular_tunes.json", "r") as f: - irregular_tunes = json.load(f) - for tune_in_idx_file in tqdm(tune_in_idx_list, total=len(tune_in_idx_list)): - if tune_in_idx_file.stem in irregular_tunes: - continue - tune_in_idx = np.load(tune_in_idx_file)['arr_0'] - tune_in_idx_dict[tune_in_idx_file.stem] = tune_in_idx - len_tunes[tune_in_idx_file.stem] = len(tune_in_idx) - file_name_list.append(tune_in_idx_file.stem) - print(f"number of loaded tunes: {len(tune_in_idx_dict)}") - return tune_in_idx_dict, len_tunes, file_name_list +# def _load_tune_in_idx(self) -> Tuple[Dict[str, np.ndarray], Dict[str, int], List[str]]: +# ''' +# Irregular tunes are removed from the dataset for better generation quality +# It includes tunes that are not quantized properly, mostly theay are expressive performance data +# ''' +# print("preprocessed tune_in_idx data is being loaded") +# tune_in_idx_list = sorted(list(Path(f"dataset/represented_data/tuneidx/tuneidx_{self.__class__.__name__}/{self.encoding_scheme}{self.num_features}").rglob("*.npz"))) +# if self.debug: +# tune_in_idx_list = tune_in_idx_list[:5000] +# tune_in_idx_dict = OrderedDict() +# len_tunes = OrderedDict() +# file_name_list = [] +# with open("metadata/LakhClean_irregular_tunes.json", "r") as f: +# irregular_tunes = json.load(f) +# for tune_in_idx_file in tqdm(tune_in_idx_list, total=len(tune_in_idx_list)): +# if tune_in_idx_file.stem in irregular_tunes: +# continue +# tune_in_idx = np.load(tune_in_idx_file)['arr_0'] +# tune_in_idx_dict[tune_in_idx_file.stem] = tune_in_idx +# len_tunes[tune_in_idx_file.stem] = len(tune_in_idx) +# file_name_list.append(tune_in_idx_file.stem) +# print(f"number of loaded tunes: {len(tune_in_idx_dict)}") +# return tune_in_idx_dict, len_tunes, file_name_list - def _get_split_list_from_tune_in_idx(self, ratio, seed): - ''' - As Lakh dataset contains multiple versions of the same song, we split the dataset based on the song name - ''' - shuffled_tune_names = list(self.tune_in_idx.keys()) - song_names_without_version = [re.sub(r"\.\d+$", "", song) for song in shuffled_tune_names] - song_dict = {} - for song, orig_song in zip(song_names_without_version, shuffled_tune_names): - if song not in song_dict: - song_dict[song] = [] - song_dict[song].append(orig_song) - unique_song_names = list(song_dict.keys()) - random.seed(seed) - random.shuffle(unique_song_names) - num_train = int(len(unique_song_names)*ratio) - num_valid = int(len(unique_song_names)*(1-ratio)/2) - train_names = [] - valid_names = [] - test_names = [] - for song_name in unique_song_names[:num_train]: - train_names.extend(song_dict[song_name]) - for song_name in unique_song_names[num_train:num_train+num_valid]: - valid_names.extend(song_dict[song_name]) - for song_name in unique_song_names[num_train+num_valid:]: - test_names.extend(song_dict[song_name]) - return train_names, valid_names, test_names, shuffled_tune_names +# def _get_split_list_from_tune_in_idx(self, ratio, seed): +# ''' +# As Lakh dataset contains multiple versions of the same song, we split the dataset based on the song name +# ''' +# shuffled_tune_names = list(self.tune_in_idx.keys()) +# song_names_without_version = [re.sub(r"\.\d+$", "", song) for song in shuffled_tune_names] +# song_dict = {} +# for song, orig_song in zip(song_names_without_version, shuffled_tune_names): +# if song not in song_dict: +# song_dict[song] = [] +# song_dict[song].append(orig_song) +# unique_song_names = list(song_dict.keys()) +# random.seed(seed) +# random.shuffle(unique_song_names) +# num_train = int(len(unique_song_names)*ratio) +# num_valid = int(len(unique_song_names)*(1-ratio)/2) +# train_names = [] +# valid_names = [] +# test_names = [] +# for song_name in unique_song_names[:num_train]: +# train_names.extend(song_dict[song_name]) +# for song_name in unique_song_names[num_train:num_train+num_valid]: +# valid_names.extend(song_dict[song_name]) +# for song_name in unique_song_names[num_train+num_valid:]: +# test_names.extend(song_dict[song_name]) +# return train_names, valid_names, test_names, shuffled_tune_names class ariamidi(SymbolicMusicDataset): def __init__(self, vocab, encoding_scheme, num_features, debug, aug_type, input_length, first_pred_feature, caption_path=None, @@ -788,6 +844,9 @@ class gigamidi(SymbolicMusicDataset): for tune_in_idx_file in tqdm(tune_in_idx_list, total=len(tune_in_idx_list)): if tune_in_idx_file.stem in irregular_tunes: continue + if "drums-only" in tune_in_idx_file.stem: + print(f"skipping {tune_in_idx_file.stem} as it is a drums-only file") + continue tune_in_idx = np.load(tune_in_idx_file)['arr_0'] tune_in_idx_dict[tune_in_idx_file.stem] = tune_in_idx len_tunes[tune_in_idx_file.stem] = len(tune_in_idx) diff --git a/Amadeus/symbolic_encoding/midi2audio.py b/Amadeus/symbolic_encoding/midi2audio.py index ddbae0f..a037a0d 100644 --- a/Amadeus/symbolic_encoding/midi2audio.py +++ b/Amadeus/symbolic_encoding/midi2audio.py @@ -11,7 +11,7 @@ License: MIT, see the LICENSE file __all__ = ['FluidSynth'] -DEFAULT_SOUND_FONT = '/data2/suhongju/research/music-generation/sound_file/CrisisGeneralMidi3.01.sf2' +DEFAULT_SOUND_FONT = 'Alex_GM.sf2' DEFAULT_SAMPLE_RATE = 48000 DEFAULT_GAIN = 0.05 # DEFAULT_SOUND_FONT = "/data2/suhongju/research/music-generation/sound_file/Advent GM 7.sf2" diff --git a/Amadeus/symbolic_yamls/config-accelerate.yaml b/Amadeus/symbolic_yamls/config-accelerate.yaml index fef73f4..5dc78b8 100644 --- a/Amadeus/symbolic_yamls/config-accelerate.yaml +++ b/Amadeus/symbolic_yamls/config-accelerate.yaml @@ -2,7 +2,8 @@ defaults: # - nn_params: nb8_embSum_NMT # - nn_params: remi8 # - nn_params: nb8_embSum_diff_t2m_150M_finetunning - - nn_params: nb8_embSum_diff_t2m_150M_pretrainingv2 + # - nn_params: nb8_embSum_diff_t2m_600M_pretrainingv2 + - nn_params: nb8_embSum_diff_t2m_600M_finetunningv2 # - nn_params: nb8_embSum_subPararell # - nn_params: nb8_embSum_diff_t2m_150M @@ -14,7 +15,7 @@ defaults: # - nn_params: remi8_main12_head_16_dim512 # - nn_params: nb5_embSum_diff_main12head16dim768_sub3 -dataset: LakhClean # Pop1k7, Pop909, SOD, LakhClean,PretrainingDataset FinetuneDataset +dataset: FinetuneDataset # Pop1k7, Pop909, SOD, LakhClean,PretrainingDataset FinetuneDataset captions_path: dataset/midicaps/train_set.json # dataset: SymphonyNet_Dataset # Pop1k7, Pop909, SOD, LakhClean @@ -30,20 +31,20 @@ tau: 0.5 train_params: device: cuda - batch_size: 3 + batch_size: 5 grad_clip: 1.0 num_iter: 300000 # total number of iterations num_cycles_for_inference: 10 # number of cycles for inference, iterations_per_validation_cycle * num_cycles_for_inference num_cycles_for_model_checkpoint: 1 # number of cycles for model checkpoint, iterations_per_validation_cycle * num_cycles_for_model_checkpoint iterations_per_training_cycle: 10 # number of iterations for logging training loss - iterations_per_validation_cycle: 5000 # number of iterations for validation process + iterations_per_validation_cycle: 3000 # number of iterations for validation process input_length: 3072 # input sequence length3072 # you can use focal loss, it it's not used, set focal_gamma to 0 focal_alpha: 1 focal_gamma: 0 # learning rate scheduler: 'cosinelr', 'cosineannealingwarmuprestarts', 'not-using', please check train_utils.py for more details scheduler : cosinelr - initial_lr: 0.00005 + initial_lr: 0.0004 decay_step_rate: 0.8 # means it will reach its lowest point at decay_step_rate * total_num_iter num_steps_per_cycle: 20000 # number of steps per cycle for 'cosineannealingwarmuprestarts' warmup_steps: 2000 #number of warmup steps diff --git a/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_150M_pretrainingv2.yaml b/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_150M_pretrainingv2.yaml index ada3f49..d7d7550 100644 --- a/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_150M_pretrainingv2.yaml +++ b/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_150M_pretrainingv2.yaml @@ -5,13 +5,13 @@ model_name: AmadeusModel input_embedder_name: SummationEmbedder main_decoder_name: XtransformerNewPretrainingDecoder sub_decoder_name: DiffusionDecoder -model_dropout: 0 +model_dropout: 0.2 input_embedder: num_layer: 1 num_head: 8 main_decoder: dim_model: 768 - num_layer: 20 + num_layer: 16 num_head: 12 sub_decoder: decout_window_size: 1 # 1 means no previous decoding output added diff --git a/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_finetunningv2.yaml b/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_finetunningv2.yaml new file mode 100644 index 0000000..90406c4 --- /dev/null +++ b/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_finetunningv2.yaml @@ -0,0 +1,19 @@ +encoding_scheme: nb +num_features: 8 +vocab_name: MusicTokenVocabNB +model_name: AmadeusModel +input_embedder_name: SummationEmbedder +main_decoder_name: XtransformerNewFinetunningDecoder +sub_decoder_name: DiffusionDecoder +model_dropout: 0 +input_embedder: + num_layer: 1 + num_head: 8 +main_decoder: + dim_model: 1024 + num_layer: 32 + num_head: 16 +sub_decoder: + decout_window_size: 1 # 1 means no previous decoding output added + num_layer: 1 + feature_enricher_use: False \ No newline at end of file diff --git a/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_pretrainingv2.yaml b/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_pretrainingv2.yaml new file mode 100644 index 0000000..9a39556 --- /dev/null +++ b/Amadeus/symbolic_yamls/nn_params/nb8_embSum_diff_t2m_600M_pretrainingv2.yaml @@ -0,0 +1,19 @@ +encoding_scheme: nb +num_features: 8 +vocab_name: MusicTokenVocabNB +model_name: AmadeusModel +input_embedder_name: SummationEmbedder +main_decoder_name: XtransformerNewPretrainingDecoder +sub_decoder_name: DiffusionDecoder +model_dropout: 0 +input_embedder: + num_layer: 1 + num_head: 8 +main_decoder: + dim_model: 1024 + num_layer: 32 + num_head: 16 +sub_decoder: + decout_window_size: 1 # 1 means no previous decoding output added + num_layer: 1 + feature_enricher_use: False \ No newline at end of file diff --git a/Amadeus/transformer_utils.py b/Amadeus/transformer_utils.py index 6fb776a..f47ee17 100644 --- a/Amadeus/transformer_utils.py +++ b/Amadeus/transformer_utils.py @@ -729,9 +729,8 @@ class XtransformerNewPretrainingDecoder(nn.Module): rotary_pos_emb = True, ff_swish = True, # set this to True ff_glu = True, # set to true to use for all feedforwards - # shift_tokens = 1, - # attn_qk_norm = True, - # attn_qk_norm_dim_scale = True + attn_gate_values = True, + attn_qk_norm = True, ) # add final dropout print('Applying Xavier Uniform Init to x-transformer following torch.Transformer') @@ -758,7 +757,7 @@ class XtransformerNewPretrainingDecoder(nn.Module): def _apply_xavier_init(self): for name, param in self.transformer_decoder.named_parameters(): - if 'to_q' in name or 'to_k' in name or 'to_v' in name: + if 'to_q' in name or 'to_k' in name or 'to_v' in name and 'to_v_gate' not in name: torch.nn.init.xavier_uniform_(param, gain=0.5**0.5) def forward(self, seq, cache=None,train=False,context=None, context_embedding=None): @@ -906,6 +905,102 @@ class XtransformerFinetuningDecoder(nn.Module): else: context = context_embedding + # concatenate context with seq + seq = torch.cat([context, seq], dim=1) # B x (T+context_length) x emb_size + if cache is not None: # implementing run_one_step in inference + if cache.hiddens is None: cache = None + hidden_vec, intermediates = self.transformer_decoder(seq, cache=cache, return_hiddens=True) + # cut to only return the seq part + return hidden_vec, intermediates + else: + if train: + hidden_vec, intermediates = self.transformer_decoder(seq, return_hiddens=True) + # cut to only return the seq part + hidden_vec = hidden_vec[:, context.shape[1]:, :] + return hidden_vec, intermediates + else: + # cut to only return the seq part + hidden_vec = self.transformer_decoder(seq) + hidden_vec = hidden_vec[:, context.shape[1]:, :] + return hidden_vec + +class XtransformerNewFinetunningDecoder(nn.Module): + def __init__( + self, + dim:int, + depth:int, + heads:int, + dropout:float + ): + super().__init__() + self._make_decoder_layer(dim, depth, heads, dropout) + self.text_encoder = T5EncoderModel.from_pretrained('google/flan-t5-base') + # frozen text encoder + for param in self.text_encoder.parameters(): + param.requires_grad = False + if dim != 768: + self.text_project = nn.Linear(768, dim) # assuming T5 base hidden size is 768 + + def _make_decoder_layer(self, dim, depth, heads, dropout): + self.transformer_decoder = Decoder( + dim = dim, + depth = depth, + heads = heads, + attn_dropout = dropout, + ff_dropout = dropout, + attn_flash = True, + use_rmsnorm=True, + rotary_pos_emb = True, + ff_swish = True, # set this to True + ff_glu = True, # set to true to use for all feedforwards + attn_gate_values = True, + attn_qk_norm = True, + ) # add final dropout + print('Applying Xavier Uniform Init to x-transformer following torch.Transformer') + self._apply_xavier_init() + print('Adding dropout after feedforward layer in x-transformer') + self._add_dropout_after_ff(dropout) + print('Adding dropout after attention layer in x-transformer') + self._add_dropout_after_attn(dropout) + + def _add_dropout_after_attn(self, dropout): + for layer in self.transformer_decoder.layers: + if 'Attention' in str(type(layer[1])): + if isinstance(layer[1].to_out, nn.Sequential): # if GLU + layer[1].to_out.append(nn.Dropout(dropout)) + elif isinstance(layer[1].to_out, nn.Linear): # if simple linear + layer[1].to_out = nn.Sequential(layer[1].to_out, nn.Dropout(dropout)) + else: + raise ValueError('to_out should be either nn.Sequential or nn.Linear') + + def _add_dropout_after_ff(self, dropout): + for layer in self.transformer_decoder.layers: + if 'FeedForward' in str(type(layer[1])): + layer[1].ff.append(nn.Dropout(dropout)) + + def _apply_xavier_init(self): + for name, param in self.transformer_decoder.named_parameters(): + if 'to_q' in name or 'to_k' in name or 'to_v' in name and 'to_v_gate' not in name: + torch.nn.init.xavier_uniform_(param, gain=0.5**0.5) + + def forward(self, seq, cache=None,train=False,context=None,context_embedding=None): + assert context is not None or context_embedding is not None, 'context or context_embedding should be provided for prefix decoder' + if context_embedding is None: + input_ids = context['input_ids'].squeeze(1) if context['input_ids'].ndim == 3 else context['input_ids'] + attention_mask = context['attention_mask'].squeeze(1) if context['attention_mask'].ndim == 3 else context['attention_mask'] + assert input_ids is not None, 'input_ids should be provided for prefix decoder' + assert attention_mask is not None, 'attention_mask should be provided for prefix decoder' + assert input_ids.device == self.text_encoder.device, 'input_ids should be on the same device as text_encoder' + + context = self.text_encoder( + input_ids=input_ids, + attention_mask=attention_mask, + ).last_hidden_state + else: + context = context_embedding + if hasattr(self, 'text_project'): + context = self.text_project(context) + # concatenate context with seq seq = torch.cat([context, seq], dim=1) # B x (T+context_length) x emb_size if cache is not None: # implementing run_one_step in inference diff --git a/data_representation/step1_midi2corpus_fined.py b/data_representation/step1_midi2corpus_fined.py index e42cbfa..49c8f25 100644 --- a/data_representation/step1_midi2corpus_fined.py +++ b/data_representation/step1_midi2corpus_fined.py @@ -113,7 +113,7 @@ class CorpusMaker(): 0 to 2000 means no limitation ''' # last_time_dict = {'BachChorale': (0, 2000), 'Pop1k7': (0, 2000), 'Pop909': (0, 2000), 'SOD': (60, 1000), 'LakhClean': (60, 600), 'Symphony': (60, 1500)} - last_time_dict = {'BachChorale': (0, 2000), 'Pop1k7': (0, 2000), 'Pop909': (0, 2000), 'SOD': (60, 1000), 'LakhClean': (0, 2000), 'Symphony': (60, 1500)} + last_time_dict = {'BachChorale': (0, 2000), 'Pop1k7': (0, 2000), 'Pop909': (0, 2000), 'SOD': (60, 1000), 'LakhClean': (8, 3000), 'Symphony': (60, 1500)} try: self.min_last_time, self.max_last_time = last_time_dict[self.dataset_name] except KeyError: diff --git a/generate-batch.py b/generate-batch.py index f43f1c0..f371b3f 100644 --- a/generate-batch.py +++ b/generate-batch.py @@ -105,6 +105,7 @@ def load_resources(wandb_exp_dir, device): config = wandb_style_config_to_omega_config(config) # Load checkpoint to specified device + print("Loading checkpoint from:", ckpt_path) ckpt = torch.load(ckpt_path, map_location=device) model, test_set, vocab = prepare_model_and_dataset_from_config(config, metadata_path, vocab_path) model.load_state_dict(ckpt['model'], strict=False) diff --git a/len_tunes/FinetuneDataset/len_nb8.png b/len_tunes/FinetuneDataset/len_nb8.png new file mode 100644 index 0000000000000000000000000000000000000000..0fe1b8731dfbe729bd82b04ddf57024cf1440bad GIT binary patch literal 24166 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 literal 0 HcmV?d00001 diff --git a/len_tunes/IrishMan/len_nb8.png b/len_tunes/IrishMan/len_nb8.png new file mode 100644 index 0000000000000000000000000000000000000000..fab79423a25bdec5927e83a3cacaa770918a17b2 GIT binary patch literal 23011 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 MJoa14!BbcM7gO5@00000 literal 0 HcmV?d00001 diff --git a/len_tunes/gigamidi/len_nb8.png b/len_tunes/gigamidi/len_nb8.png new file mode 100644 index 0000000000000000000000000000000000000000..cba11fcc8ef8cb26ff83316bbca6f134a09fa396 GIT binary patch literal 24892 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 [options] + +This script traverses a directory and all subdirectories to find MID files, +extracts musical features from each file using multi-threading for speed, +and saves the results to CSV files. +""" + +import argparse +import pathlib +import os +import csv +import json +from multiprocessing import Pool +from itertools import chain +from math import ceil +from functools import partial + +import numpy as np +from numpy.lib.stride_tricks import sliding_window_view +from symusic import Score +import pandas as pd +from tqdm import tqdm +from numba import njit, prange + + +@njit +def merge_intervals(intervals: list[tuple[int, int]], threshold: int): + """Merge overlapping or close intervals.""" + out = [] + last_s, last_e = intervals[0] + + for i in range(1, len(intervals)): + s, e = intervals[i] + + if s - last_e <= threshold: + if e > last_e: + last_e = e + else: + out.append((last_s, last_e)) + last_s, last_e = s, e + + out.append((last_s, last_e)) + return out + + +@njit(fastmath=True) +def note_distribution(events: list[tuple[float, int]], threshold: int = 2, segment_threshold: int = 0): + """Calculate polyphony rate and sounding segments.""" + try: + events.sort() + active_notes = 0 + polyphonic_steps = 0 + total_steps = 0 + last_time = None + last_state = False + last_seg_start = 0 + sounding_segments = [] + + for time, change in events: + if last_time is not None and time != last_time: + if active_notes >= threshold: + polyphonic_steps += (time - last_time) + if active_notes: + total_steps += (time - last_time) + if(last_state != bool(active_notes)): + if(last_state): + last_seg_start = time + else: + sounding_segments.append((last_seg_start, time)) + + active_notes += change + last_state = bool(active_notes) + last_time = time + + if(segment_threshold != 0): + sounding_segments = merge_intervals(sounding_segments, segment_threshold) + + return polyphonic_steps / total_steps, total_steps, sounding_segments + except: + return None, None, None + + +@njit(fastmath=True) +def entropy(X: np.ndarray, base: float = 2.0) -> float: + """Calculate entropy function optimized with numba.""" + N, M = X.shape + out = np.empty(N, dtype=np.float64) + log_base = np.log(base) if base > 0.0 else 1.0 + + for i in prange(N): + row = X[i] + total = np.nansum(row) + if total <= 0.0: + out[i] = 0.0 + continue + + mask = (~np.isnan(row)) & (row > 0.0) + probs = row[mask] / total + if probs.size == 0: + out[i] = 0.0 + else: + H = -np.sum(probs * np.log(probs)) + if base > 0.0: + H /= log_base + out[i] = H + + nz = out > 0.0 + if not np.any(nz): + return 0.0 + return float(np.exp(np.mean(np.log(out[nz])))) + + +@njit(fastmath=True) +def n_gram_co_occurence_entropy(seq: list[list[int]], N: int = 5): + """Calculate n-gram co-occurrence entropy.""" + counts = [] + + for seg in seq: + if len(seg) < 2: + continue + + arr = np.asarray(seg, dtype=np.int64) + + min_val = np.min(arr) + if min_val < 0: + arr = arr - min_val + + vocabs = int(np.max(arr) + 1) + + wlen = N if len(arr) >= N else len(arr) + nwin = len(arr) - wlen + 1 + + C = np.zeros((vocabs, vocabs), dtype=np.int64) + + for start in range(nwin): + for i in range(wlen - 1): + a = int(arr[start + i]) + for j in range(i + 1, wlen): + b = int(arr[start + j]) + if a < vocabs and b < vocabs: + C[a, b] += 1 + + for i in range(vocabs): + counts.append(int(C[i, i])) + for j in range(i + 1, vocabs): + counts.append(int(C[i, j])) + + total = 0 + for v in counts: + total += v + + if total <= 0: + return 0.0 + + H = 0.0 + for v in counts: + if v > 0: + p = v / total + H -= p * np.log(p) + + return H + + +def calc_pitch_distribution(pitches: np.ndarray, window_size: int = 32, hop_size: int = 16): + """Calculate pitch distribution features.""" + sw = (lambda x: sliding_window_view(x, window_size)[::hop_size, :]) if len(pitches) > window_size else (lambda x: x.reshape(1, -1)) + + used_pitches = np.unique(pitches) + n_pitches_used = len(used_pitches) + pitch_entropy = entropy(sw(pitches)) + pitch_range = [int(min(used_pitches)), int(max(used_pitches))] + + pitch_classes = pitches % 12 + n_pitch_classes_used = len(np.unique(pitch_classes)) + pitch_class_entropy = entropy(sw(pitch_classes)) + + return n_pitch_classes_used, n_pitches_used, pitch_class_entropy, pitch_entropy, pitch_range + + +def calc_rhythmic_entropy(ioi: np.ndarray, window_size: int = 32, hop_size: int = 16): + """Calculate rhythmic entropy.""" + sw = (lambda x: sliding_window_view(x, window_size)[::hop_size, :]) if len(ioi) > window_size else (lambda x: x.reshape(1, -1)) + if(len(ioi) == 0): + return None + return entropy(sw(ioi)) + + +def extract_features(midi_path: pathlib.Path, tpq: int = 6): + """Extract features from a single MIDI file.""" + try: + seg_threshold = tpq * 8 + midi_id = midi_path.parent.name + '/' + midi_path.stem + score = Score(midi_path).resample(tpq) + + track_features = [] + for i, t in enumerate(score.tracks): + if(not len(t.notes)): + track_features.append(( + midi_id, # midi_id + i, # track_id + 128 if t.is_drum else t.program, # instrument + + 0, # end_time + 0, # note_num + None, # sounding_interval + + None, # note_density + None, # polyphony_rate + None, # rhythmic_entropy + None, # rhythmic_token_co_occurrence_entropy + + None, # n_pitch_classes_used + None, # n_pitches_used + None, # pitch_class_entropy + None, # pitch_entropy + None, # pitch_range + None # interval_token_co_occurrence_entropy + )) + continue + t.sort() + + features = t.notes.numpy() + + ioi = np.diff(features['time']) + seg_points = np.where(ioi > tpq * seg_threshold)[0] + + polyphony_rate, sounding_interval_length, sounding_segment = note_distribution(list(chain(* + [((note.start, 1), (note.end, -1)) for note in t.notes]))) + rhythmic_entropy = calc_rhythmic_entropy(ioi) + + rhythmic_token_co_occurrence_entropy = n_gram_co_occurence_entropy([i for i in np.split(ioi, seg_points) if np.all(i) <= seg_threshold]) + + if(t.is_drum or len(t.notes) < 2): + track_features.append(( + midi_id, # midi_id + i, # track_id + 128 if t.is_drum else t.program, # instrument + + t.end(), # end_time + len(t.notes), # note_num + sounding_interval_length, # sounding_interval + + len(t.notes) / ceil(sounding_interval_length) if sounding_interval_length else None, # note_density + polyphony_rate, # polyphony_rate + rhythmic_entropy, # rhythmic_entropy + rhythmic_token_co_occurrence_entropy, # rhythmic_token_co_occurrence_entropy + + None, # n_pitch_classes_used + None, # n_pitches_used + None, # pitch_class_entropy + None, # pitch_entropy + None, # pitch_range + None # interval_token_co_occurrence_entropy + )) + else: + n_pitch_classes_used, n_pitches_used, pitch_class_entropy, pitch_entropy, pitch_range = calc_pitch_distribution(features['pitch']) + intervals = np.diff(features['pitch']) + track_features.append(( + midi_id, # midi_id + i, # track_id + t.program, # instrument + + t.end(), # end_time + len(t.notes), # note_num + sounding_interval_length, # sounding_interval + + len(t.notes) / ceil(sounding_interval_length) if sounding_interval_length else None, # note_density + polyphony_rate, # polyphony_rate + rhythmic_entropy, # rhythmic_entropy + rhythmic_token_co_occurrence_entropy, # rhythmic_token_co_occurrence_entropy + + n_pitch_classes_used, # n_pitch_classes_used + n_pitches_used, # n_pitches_used + pitch_class_entropy, # pitch_class_entropy + pitch_entropy, # pitch_entropy + json.dumps(pitch_range), # pitch_range + n_gram_co_occurence_entropy([p for i, p in zip(np.split(ioi, seg_points), np.split(intervals, seg_points)) if np.all(i) <= seg_threshold]) # interval_token_co_occurrence_entropy + )) + + score_features = ( + midi_id, # midi_id + sum(tf[4] for tf in track_features) if track_features else 0, # note_num + max(tf[3] for tf in track_features) if track_features else 0, # end_time + json.dumps([[ks.time, ks.key, ks.tonality] for ks in score.key_signatures]), # key + json.dumps([[ts.time, ts.numerator, ts.denominator] for ts in score.time_signatures]), # time_signature + json.dumps([[t.time, t.qpm] for t in score.tempos]) # tempo + ) + + return score_features, track_features + except Exception as e: + print(f"Error processing {midi_path}: {e}") + return None, None + + +def find_midi_files(directory: pathlib.Path): + """Find all MIDI files in directory and subdirectories.""" + midi_extensions = {'.mid', '.midi', '.MID', '.MIDI'} + midi_files = [] + + # Use rglob to recursively find MIDI files + for file_path in directory.rglob('*'): + if file_path.is_file() and file_path.suffix in midi_extensions: + midi_files.append(file_path) + + return midi_files + + +def process_midi_files(directory: pathlib.Path, output_prefix: str = "midi_features", + num_threads: int = 4, tpq: int = 6): + """Process MIDI files with multi-threading and save to CSV.""" + + # Find all MIDI files + print(f"Searching for MIDI files in: {directory}") + midi_files = find_midi_files(directory) + + if not midi_files: + print(f"No MIDI files found in {directory}") + return + + print(f"Found {len(midi_files)} MIDI files") + + # Create extractor function with fixed parameters + extractor = partial(extract_features, tpq=tpq) + + # Feature column names + score_feat_cols = ['midi_id', 'note_num', 'end_time', 'key', 'time_signature', 'tempo'] + track_feat_cols = ['midi_id', 'track_id', 'instrument', 'end_time', 'note_num', + 'sounding_interval', 'note_density', 'polyphony_rate', 'rhythmic_entropy', + 'rhythmic_token_co_occurrence_entropy', 'n_pitch_classes_used', + 'n_pitches_used', 'pitch_class_entropy', 'pitch_entropy', 'pitch_range', + 'interval_token_co_occurrence_entropy'] + + # Process files with multiprocessing + print(f"Processing files with {num_threads} threads...") + + with Pool(num_threads) as pool: + # Open CSV files for writing + with open(f'{output_prefix}_score_features.csv', 'w', newline='', encoding='utf-8') as score_csvfile: + score_writer = csv.writer(score_csvfile) + score_writer.writerow(score_feat_cols) + + with open(f'{output_prefix}_track_features.csv', 'w', newline='', encoding='utf-8') as track_csvfile: + track_writer = csv.writer(track_csvfile) + track_writer.writerow(track_feat_cols) + + # Process files with progress bar + processed_count = 0 + skipped_count = 0 + + for score_feat, track_feats in tqdm(pool.imap_unordered(extractor, midi_files), + total=len(midi_files), + desc="Processing MIDI files"): + if not (score_feat, track_feats): + skipped_count += 1 + continue + + processed_count += 1 + + # Write score features + score_writer.writerow(score_feat) + + # Write track features + if track_feats: + track_writer.writerows(track_feats) + + print(f"\nProcessing complete!") + print(f"Successfully processed: {processed_count} files") + print(f"Skipped due to errors: {skipped_count} files") + print(f"Score features saved to: {output_prefix}_score_features.csv") + print(f"Track features saved to: {output_prefix}_track_features.csv") + + +def main(): + """Main function with command line argument parsing.""" + parser = argparse.ArgumentParser( + description="Extract musical features from MIDI files and save to CSV", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python midi_statistics.py /path/to/midi/files + python midi_statistics.py /path/to/midi/files --threads 8 --output my_features + python midi_statistics.py /path/to/midi/files --tpq 12 --threads 2 + +Features extracted: + - Score level: note count, end time, key signatures, time signatures, tempo + - Track level: instrument, note density, polyphony rate, rhythmic entropy, + pitch distribution, and more + """ + ) + + parser.add_argument('directory', + help='Path to directory containing MIDI files') + + parser.add_argument('--threads', '-t', + type=int, + default=4, + help='Number of threads to use (default: 4)') + + parser.add_argument('--output', '-o', + type=str, + default='midi_features', + help='Output file prefix (default: midi_features)') + + parser.add_argument('--tpq', + type=int, + default=6, + help='Ticks per quarter note for resampling (default: 6)') + + args = parser.parse_args() + + # Validate directory + directory = pathlib.Path(args.directory) + if not directory.exists(): + print(f"Error: Directory '{directory}' does not exist") + return 1 + + if not directory.is_dir(): + print(f"Error: '{directory}' is not a directory") + return 1 + + # Validate threads + if args.threads < 1: + print("Error: Number of threads must be at least 1") + return 1 + + try: + process_midi_files(directory, args.output, args.threads, args.tpq) + return 0 + except KeyboardInterrupt: + print("\nProcessing interrupted by user") + return 1 + except Exception as e: + print(f"Error: {e}") + return 1 + + +if __name__ == "__main__": + exit(main()) diff --git a/,idi_sim.py b/,idi_sim.py new file mode 100644 index 0000000..dfe9488 --- /dev/null +++ b/,idi_sim.py @@ -0,0 +1,105 @@ +import os +import numpy as np +import pandas as pd +from symusic import Score +from concurrent.futures import ProcessPoolExecutor, as_completed + +semitone2degree = np.array([0, 2, 2, 3, 3, 4, 4.5, 4, 3, 3, 2, 2]) + +def hausdorff_dist(a: np.ndarray, b: np.ndarray, weight: tuple[float, float] = (2., 1.5), oti: bool = True): + if(not a.shape[1] or not b.shape[1]): + return np.inf + a_onset, a_pitch = a + b_onset, b_pitch = b + a_onset = a_onset.astype(np.float32) + b_onset = b_onset.astype(np.float32) + a_pitch = a_pitch.astype(np.uint8) + b_pitch = b_pitch.astype(np.uint8) + + onset_dist_matrix = np.abs(a_onset.reshape(1, -1) - b_onset.reshape(-1, 1)) + if(oti): + pitch_dist_matrix = semitone2degree[np.abs(a_pitch.reshape(1, 1, -1) + np.arange(12).reshape(-1, 1, 1) - b_pitch.reshape(-1, 1)) % 12] + dist_matrix = (weight[0] * np.expand_dims(onset_dist_matrix, 0) + weight[1] * pitch_dist_matrix) / sum(weight) + a2b = dist_matrix.min(2) + b2a = dist_matrix.min(1) + dist = np.concatenate([a2b, b2a], axis=1) + return dist.sum(axis=1).min() / len(dist) + else: + pitch_dist_matrix = semitone2degree[np.abs(a_pitch.reshape(1, -1) - b_pitch.reshape(-1, 1)) % 12] + dist_matrix = (weight[0] * onset_dist_matrix + weight[1] * pitch_dist_matrix) / sum(weight) + a2b = dist_matrix.min(1) + b2a = dist_matrix.min(0) + return float((a2b.sum() + b2a.sum()) / (a.shape[1] + b.shape[1])) + + +def midi_time_sliding_window(x: list[tuple[float, int]], window_size: float = 16., hop_size: float = 4.): + x = sorted(x) + end_time = x[-1][0] + out = [[] for _ in range(int(end_time // hop_size))] + for i in sorted(x): + segment = min(int(i[0] // hop_size), len(out) - 1) + while(i[0] >= segment * hop_size): + out[segment].append(i) + segment -= 1 + if(segment < 0): + break + return out + + +def midi_dist(a: list[tuple[float, int]], b: list[tuple[float, int]], window_size: float = 16., hop_size: float = 4): + a = midi_time_sliding_window(a) + b = midi_time_sliding_window(b) + dist = np.inf + for i in a: + for j in b: + cur_dist = hausdorff_dist(np.array(i, dtype=np.float32).T, np.array(j, dtype=np.float32).T) + if(cur_dist < dist): + dist = cur_dist + return dist + + +def extract_notes(filepath: str): + """读取MIDI并返回 (time, pitch) 列表""" + try: + s = Score(filepath).to("quarter") + notes = [] + for t in s.tracks: + notes.extend([(n.time, n.pitch) for n in t.notes]) + return notes + except Exception as e: + print(f"读取 {filepath} 出错: {e}") + return [] + + +def compare_pair(file_a: str, file_b: str): + notes_a = extract_notes(file_a) + notes_b = extract_notes(file_b) + if not notes_a or not notes_b: + return (file_a, file_b, np.inf) + dist = midi_dist(notes_a, notes_b) + return (file_a, file_b, dist) + + +def batch_compare(dir_a: str, dir_b: str, out_csv: str = "midi_similarity.csv", max_workers: int = 8): + files_a = [os.path.join(dir_a, f) for f in os.listdir(dir_a) if f.endswith(".mid")] + files_b = [os.path.join(dir_b, f) for f in os.listdir(dir_b) if f.endswith(".mid")] + + results = [] + with ProcessPoolExecutor(max_workers=max_workers) as executor: + futures = [executor.submit(compare_pair, fa, fb) for fa in files_a for fb in files_b] + for fut in as_completed(futures): + results.append(fut.result()) + + # 排序 + results = sorted(results, key=lambda x: x[2]) + + # 保存 + df = pd.DataFrame(results, columns=["file_a", "file_b", "distance"]) + df.to_csv(out_csv, index=False) + print(f"已保存结果到 {out_csv}") + + +if __name__ == "__main__": + dir_a = "folder_a" + dir_b = "folder_b" + batch_compare(dir_a, dir_b, out_csv="midi_similarity.csv", max_workers=8) \ No newline at end of file