Spaces:
Build error
Build error
Hugo Flores Garcia commited on
Commit ·
3a788dd
1
Parent(s): b88d0ca
add bytecover
Browse files- app.py +222 -9
- bytecover/__init__.py +0 -0
- bytecover/__main__.py +17 -0
- bytecover/config.yaml +51 -0
- bytecover/config_gpu.yaml +51 -0
- bytecover/models/__init__.py +0 -0
- bytecover/models/data_loader.py +152 -0
- bytecover/models/data_model.py +39 -0
- bytecover/models/early_stopper.py +19 -0
- bytecover/models/modules.py +200 -0
- bytecover/models/train_module.py +319 -0
- bytecover/models/utils.py +119 -0
- bytecover/utils.py +36 -0
- orfium-bytecover.pt +3 -0
- pinecone_generate.py +275 -0
- requirements.txt +20 -2
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import spaces
|
| 2 |
from pathlib import Path
|
| 3 |
import yaml
|
| 4 |
import time
|
|
@@ -15,6 +15,191 @@ import gradio as gr
|
|
| 15 |
from vampnet.interface import Interface, signal_concat
|
| 16 |
from vampnet import mask as pmask
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 19 |
|
| 20 |
interface = Interface.default()
|
|
@@ -57,7 +242,6 @@ def shift_pitch(signal, interval: int):
|
|
| 57 |
return signal
|
| 58 |
|
| 59 |
|
| 60 |
-
@spaces.GPU
|
| 61 |
def _vamp(
|
| 62 |
seed, input_audio, model_choice,
|
| 63 |
pitch_shift_amt, periodic_p,
|
|
@@ -168,7 +352,7 @@ def api_vamp(data):
|
|
| 168 |
|
| 169 |
OUT_DIR = Path("gradio-outputs")
|
| 170 |
OUT_DIR.mkdir(exist_ok=True)
|
| 171 |
-
def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks):
|
| 172 |
sig = at.AudioSignal(input_audio_file)
|
| 173 |
sr, samples = sig.sample_rate, sig.samples[0][0].detach().cpu().numpy()
|
| 174 |
# convert to int32
|
|
@@ -192,7 +376,11 @@ def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks):
|
|
| 192 |
stretch_factor=1,
|
| 193 |
)
|
| 194 |
|
| 195 |
-
sig = at.AudioSignal(samples, sr)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
# write to file
|
| 197 |
# clear the outdir
|
| 198 |
for p in OUT_DIR.glob("*"):
|
|
@@ -200,10 +388,7 @@ def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks):
|
|
| 200 |
OUT_DIR.mkdir(exist_ok=True)
|
| 201 |
outpath = OUT_DIR / f"{uuid.uuid4()}.wav"
|
| 202 |
sig.write(outpath)
|
| 203 |
-
|
| 204 |
-
output_labels = LabelList()
|
| 205 |
-
output_labels.append(AudioLabel(label='~', t=0.0, amplitude=0.5, description='generated audio'))
|
| 206 |
-
return outpath, output_labels
|
| 207 |
|
| 208 |
|
| 209 |
with gr.Blocks() as demo:
|
|
@@ -425,19 +610,47 @@ with gr.Blocks() as demo:
|
|
| 425 |
|
| 426 |
from pyharp import ModelCard, build_endpoint
|
| 427 |
card = ModelCard(
|
| 428 |
-
name="vampnet",
|
| 429 |
description="vampnet! is a model for generating audio from audio",
|
| 430 |
author="hugo flores garcía",
|
| 431 |
tags=["music generation"],
|
| 432 |
midi_in=False,
|
| 433 |
midi_out=False
|
| 434 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
|
| 436 |
# Build a HARP-compatible endpoint
|
| 437 |
app = build_endpoint(model_card=card,
|
| 438 |
components=[
|
| 439 |
periodic_p,
|
| 440 |
n_mask_codebooks,
|
|
|
|
| 441 |
],
|
| 442 |
process_fn=harp_vamp)
|
| 443 |
|
|
|
|
| 1 |
+
# import spaces
|
| 2 |
from pathlib import Path
|
| 3 |
import yaml
|
| 4 |
import time
|
|
|
|
| 15 |
from vampnet.interface import Interface, signal_concat
|
| 16 |
from vampnet import mask as pmask
|
| 17 |
|
| 18 |
+
from pyharp import *
|
| 19 |
+
from bytecover.models.train_module import TrainModule
|
| 20 |
+
from bytecover.utils import initialize_logging, load_config
|
| 21 |
+
import pinecone
|
| 22 |
+
import laion_clap
|
| 23 |
+
from tqdm import tqdm
|
| 24 |
+
|
| 25 |
+
import os
|
| 26 |
+
|
| 27 |
+
### INIT BYTECOVER
|
| 28 |
+
print(f"Is CUDA available: {torch.cuda.is_available()}")
|
| 29 |
+
# True
|
| 30 |
+
print(f"CUDA device: {torch.cuda.get_device_name(torch.cuda.current_device())}")
|
| 31 |
+
|
| 32 |
+
index_clap = pinecone.Index(os.environ["PC_API_KEY"], host=os.environ["CLAP_INDEX"]) #host='https://clap-nathan-500-index-af8053a.svc.us-west1-gcp.pinecone.io')
|
| 33 |
+
index_bytecover = pinecone.Index(os.environ["PC_API_KEY"], host=os.environ["BC_INDEX"]) #host='https://bytecover-nathan-500-index-af8053a.svc.us-west1-gcp.pinecone.io')
|
| 34 |
+
|
| 35 |
+
print("Loading ByteCover model")
|
| 36 |
+
|
| 37 |
+
if torch.cuda.is_available():
|
| 38 |
+
bytecover_config = load_config(config_path="bytecover/config_gpu.yaml")
|
| 39 |
+
else:
|
| 40 |
+
bytecover_config = load_config(config_path="bytecover/config.yaml")
|
| 41 |
+
bytecover_module = TrainModule(bytecover_config)
|
| 42 |
+
bytecover_model = bytecover_module.model
|
| 43 |
+
if bytecover_module.best_model_path is not None:
|
| 44 |
+
bytecover_model.load_state_dict(torch.load(bytecover_module.best_model_path), strict=False)
|
| 45 |
+
print(f"Best model loaded from checkpoint: {bytecover_module.best_model_path}")
|
| 46 |
+
elif bytecover_module.config["test"]["model_ckpt"] is not None:
|
| 47 |
+
bytecover_model.load_state_dict(torch.load(bytecover_module.config["test"]["model_ckpt"], map_location='cpu'), strict=False)
|
| 48 |
+
print(f'Model loaded from checkpoint: {bytecover_module.config["test"]["model_ckpt"]}')
|
| 49 |
+
elif bytecover_module.state == "initializing":
|
| 50 |
+
print("Warning: Running with random weights")
|
| 51 |
+
|
| 52 |
+
bytecover_model.eval()
|
| 53 |
+
|
| 54 |
+
print("Loading CLAP model")
|
| 55 |
+
|
| 56 |
+
if torch.cuda.is_available():
|
| 57 |
+
clap_model = laion_clap.CLAP_Module(enable_fusion=False, device="cuda:0")
|
| 58 |
+
else:
|
| 59 |
+
clap_model = laion_clap.CLAP_Module(enable_fusion=False)
|
| 60 |
+
clap_model.load_ckpt() # download the default pretrained checkpoint.
|
| 61 |
+
|
| 62 |
+
print("Models loaded!")
|
| 63 |
+
def convert_to_npfloat64(original_array):
|
| 64 |
+
#return np.array(flat_df["flat_vector_embed"][0],dtype=np.float64)
|
| 65 |
+
return np.array(original_array,dtype=np.float64)
|
| 66 |
+
|
| 67 |
+
def convert_to_npfloat64_to_list(vector_embed_64):
|
| 68 |
+
# list(flat_df["flat_vector_embed_64"][0])
|
| 69 |
+
return list(vector_embed_64)
|
| 70 |
+
|
| 71 |
+
def flatten_vector_embed(vector_embed):
|
| 72 |
+
return list(vector_embed.flatten())
|
| 73 |
+
|
| 74 |
+
def format_time(num_seconds):
|
| 75 |
+
return f"{num_seconds // 60}:{num_seconds % 60:02d}"
|
| 76 |
+
|
| 77 |
+
def bytecover(sig, chunk_size=3.0, bytecover_match_ct=3, clap_match_ct=3):
|
| 78 |
+
"""
|
| 79 |
+
This function defines the audio processing steps
|
| 80 |
+
Args:
|
| 81 |
+
input_audio_path (str): the audio filepath to be processed.
|
| 82 |
+
<YOUR_KWARGS>: additional keyword arguments necessary for processing.
|
| 83 |
+
NOTE: These should correspond to and match order of UI elements defined below.
|
| 84 |
+
Returns:
|
| 85 |
+
output_audio_path (str): the filepath of the processed audio.
|
| 86 |
+
output_labels (LabelList): any labels to display.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
"""
|
| 90 |
+
<YOUR AUDIO LOADING CODE HERE>
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
"""
|
| 95 |
+
<YOUR AUDIO PROCESSING CODE HERE>
|
| 96 |
+
"""
|
| 97 |
+
sig_mono = sig.copy().to_mono().audio_data.squeeze(1)
|
| 98 |
+
|
| 99 |
+
# Chunk audio to desired length
|
| 100 |
+
|
| 101 |
+
chunk_samples = int(chunk_size * sig.sample_rate)
|
| 102 |
+
print(f"Chunk samples: {chunk_samples}")
|
| 103 |
+
print(f"Shape of audio: {sig_mono.shape}")
|
| 104 |
+
chunks = torch.tensor_split(sig_mono, [i for i in range(chunk_samples, sig_mono.shape[1], chunk_samples)], dim=1)
|
| 105 |
+
if chunks[-1].shape[1] < chunk_samples:
|
| 106 |
+
print("Cutting last chunk due to length")
|
| 107 |
+
chunks = tuple(list(chunks)[:-1])
|
| 108 |
+
print(f"Number of chunks: {len(chunks)}")
|
| 109 |
+
|
| 110 |
+
print("Getting Bytecover embeddings")
|
| 111 |
+
bytecover_embeddings = []
|
| 112 |
+
for chunk in tqdm(chunks):
|
| 113 |
+
result = bytecover_model.forward(chunk.to(bytecover_module.config["device"]))['f_t'].detach()
|
| 114 |
+
bytecover_embeddings.append(result)
|
| 115 |
+
|
| 116 |
+
clean_bytecover_embeddings = [convert_to_npfloat64_to_list(convert_to_npfloat64(flatten_vector_embed(embedding.cpu()))) for embedding in bytecover_embeddings]
|
| 117 |
+
|
| 118 |
+
print("Getting CLAP embeddings")
|
| 119 |
+
clap_embeddings = []
|
| 120 |
+
for chunk in tqdm(chunks):
|
| 121 |
+
result = clap_model.get_audio_embedding_from_data(chunk.numpy())
|
| 122 |
+
clap_embeddings.append(result)
|
| 123 |
+
|
| 124 |
+
clean_clap_embeddings = [convert_to_npfloat64_to_list(convert_to_npfloat64(flatten_vector_embed(embedding))) for embedding in clap_embeddings]
|
| 125 |
+
|
| 126 |
+
clap_matches = []
|
| 127 |
+
bytecover_matches = []
|
| 128 |
+
match_metadatas = {}
|
| 129 |
+
|
| 130 |
+
output_labels = LabelList()
|
| 131 |
+
|
| 132 |
+
times = {}
|
| 133 |
+
|
| 134 |
+
for clean_embeddings, pinecone_index, match_list, embedding_num, num_matches in zip([clean_bytecover_embeddings, clean_clap_embeddings], [index_bytecover, index_clap], [bytecover_matches, clap_matches], range(2), [bytecover_match_ct, clap_match_ct]):
|
| 135 |
+
|
| 136 |
+
for i, embedding in enumerate(clean_embeddings):
|
| 137 |
+
|
| 138 |
+
print(f"Getting match {i + 1} of {len(clean_embeddings)}")
|
| 139 |
+
matches = pinecone_index.query(
|
| 140 |
+
vector=embedding,
|
| 141 |
+
top_k=10,
|
| 142 |
+
#include_values=False,
|
| 143 |
+
include_metadata=True
|
| 144 |
+
)['matches']
|
| 145 |
+
|
| 146 |
+
# Store matches as [score, time, id]
|
| 147 |
+
|
| 148 |
+
for match in matches:
|
| 149 |
+
id = match['id']
|
| 150 |
+
if id not in match_metadatas:
|
| 151 |
+
match_metadatas[id] = match['metadata']
|
| 152 |
+
match_list.append([match['score'], i * chunk_size, id])
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
print("Matches obtained!")
|
| 156 |
+
|
| 157 |
+
top_matches = sorted(match_list, key=lambda item: item[0], reverse=True)
|
| 158 |
+
|
| 159 |
+
for i, match in enumerate(top_matches[:int(num_matches)]):
|
| 160 |
+
metadata = match_metadatas[match[2]]
|
| 161 |
+
song_artists = metadata['artists']
|
| 162 |
+
if type(song_artists) is list:
|
| 163 |
+
artists = ' and '.join(artists)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
song_title = metadata['song']
|
| 167 |
+
|
| 168 |
+
song_link = f"https://open.spotify.com/track/{metadata['spotify_id'].split(':')[2]}"
|
| 169 |
+
|
| 170 |
+
embed_name = ['ByteCover', 'CLAP'][embedding_num]
|
| 171 |
+
|
| 172 |
+
match_time = match[1]
|
| 173 |
+
times[match_time] = times.get(match_time, 0) + 1
|
| 174 |
+
|
| 175 |
+
label = AudioLabel(
|
| 176 |
+
t=match_time,
|
| 177 |
+
label=f'{song_title}',
|
| 178 |
+
duration=chunk_size,
|
| 179 |
+
link=song_link,
|
| 180 |
+
description=f'Embedding: {embed_name}\n{song_title} by {song_artists}\nClick the tag to view on Spotify!',
|
| 181 |
+
amplitude=1.0 - 0.5 * (times[match_time] - 1),
|
| 182 |
+
color=AudioLabel.rgb_color_to_int(200, 170, 3, 10) if embedding_num == 1 else 0
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# if embedding_num == 1:
|
| 186 |
+
# label.rgb_color_to_int(200, 170, 3, 240)
|
| 187 |
+
# else:
|
| 188 |
+
# pass
|
| 189 |
+
# #label.set_color(204, 52, 235, 240)
|
| 190 |
+
|
| 191 |
+
output_labels.append(label)
|
| 192 |
+
|
| 193 |
+
"""
|
| 194 |
+
<YOUR AUDIO SAVING CODE HERE>
|
| 195 |
+
# Save processed audio and obtain default path
|
| 196 |
+
output_audio_path = save_audio(signal, None)
|
| 197 |
+
"""
|
| 198 |
+
|
| 199 |
+
return output_labels
|
| 200 |
+
|
| 201 |
+
### END BYTECOVER
|
| 202 |
+
|
| 203 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 204 |
|
| 205 |
interface = Interface.default()
|
|
|
|
| 242 |
return signal
|
| 243 |
|
| 244 |
|
|
|
|
| 245 |
def _vamp(
|
| 246 |
seed, input_audio, model_choice,
|
| 247 |
pitch_shift_amt, periodic_p,
|
|
|
|
| 352 |
|
| 353 |
OUT_DIR = Path("gradio-outputs")
|
| 354 |
OUT_DIR.mkdir(exist_ok=True)
|
| 355 |
+
def harp_vamp(input_audio_file, periodic_p, n_mask_codebooks, chunk_size=3.0, bytecover_match_ct=3, clap_match_ct=3):
|
| 356 |
sig = at.AudioSignal(input_audio_file)
|
| 357 |
sr, samples = sig.sample_rate, sig.samples[0][0].detach().cpu().numpy()
|
| 358 |
# convert to int32
|
|
|
|
| 376 |
stretch_factor=1,
|
| 377 |
)
|
| 378 |
|
| 379 |
+
sig = at.AudioSignal(samples, sr).cpu()
|
| 380 |
+
|
| 381 |
+
# run bytecover
|
| 382 |
+
labels = bytecover(sig, chunk_size, bytecover_match_ct, clap_match_ct)
|
| 383 |
+
|
| 384 |
# write to file
|
| 385 |
# clear the outdir
|
| 386 |
for p in OUT_DIR.glob("*"):
|
|
|
|
| 388 |
OUT_DIR.mkdir(exist_ok=True)
|
| 389 |
outpath = OUT_DIR / f"{uuid.uuid4()}.wav"
|
| 390 |
sig.write(outpath)
|
| 391 |
+
return outpath, labels
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
|
| 394 |
with gr.Blocks() as demo:
|
|
|
|
| 610 |
|
| 611 |
from pyharp import ModelCard, build_endpoint
|
| 612 |
card = ModelCard(
|
| 613 |
+
name="vampnet + aitribution",
|
| 614 |
description="vampnet! is a model for generating audio from audio",
|
| 615 |
author="hugo flores garcía",
|
| 616 |
tags=["music generation"],
|
| 617 |
midi_in=False,
|
| 618 |
midi_out=False
|
| 619 |
)
|
| 620 |
+
|
| 621 |
+
# BYTECOVER
|
| 622 |
+
# Define Gradio Components
|
| 623 |
+
components = [
|
| 624 |
+
# <YOUR UI ELEMENTS HERE>
|
| 625 |
+
gr.Slider(
|
| 626 |
+
minimum=1.0,
|
| 627 |
+
maximum=10.0,
|
| 628 |
+
step=0.5,
|
| 629 |
+
value=3.0,
|
| 630 |
+
label="Sample size (s)"
|
| 631 |
+
),
|
| 632 |
+
gr.Slider(
|
| 633 |
+
minimum=0,
|
| 634 |
+
maximum=5,
|
| 635 |
+
step=1,
|
| 636 |
+
value=3,
|
| 637 |
+
label="Bytecover matches to generate"
|
| 638 |
+
),
|
| 639 |
+
gr.Slider(
|
| 640 |
+
minimum=0,
|
| 641 |
+
maximum=5,
|
| 642 |
+
step=1,
|
| 643 |
+
value=3,
|
| 644 |
+
label="CLAP matches to generate"
|
| 645 |
+
)
|
| 646 |
+
]
|
| 647 |
|
| 648 |
# Build a HARP-compatible endpoint
|
| 649 |
app = build_endpoint(model_card=card,
|
| 650 |
components=[
|
| 651 |
periodic_p,
|
| 652 |
n_mask_codebooks,
|
| 653 |
+
*components
|
| 654 |
],
|
| 655 |
process_fn=harp_vamp)
|
| 656 |
|
bytecover/__init__.py
ADDED
|
File without changes
|
bytecover/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import wandb
|
| 2 |
+
|
| 3 |
+
from bytecover.models.train_module import TrainModule
|
| 4 |
+
from bytecover.utils import initialize_logging, load_config
|
| 5 |
+
|
| 6 |
+
config = load_config(config_path="config/config.yaml")
|
| 7 |
+
initialize_logging(config_path="config/logging_config.yaml", debug=False)
|
| 8 |
+
if config["wandb"]:
|
| 9 |
+
wandb.init(
|
| 10 |
+
# set the wandb project where this run will be logged
|
| 11 |
+
project="ByteCover",
|
| 12 |
+
# track hyperparameters and run metadata
|
| 13 |
+
config=config["train"],
|
| 14 |
+
)
|
| 15 |
+
trainer = TrainModule(config)
|
| 16 |
+
trainer.pipeline()
|
| 17 |
+
trainer.test()
|
bytecover/config.yaml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data_path: bytecover_data
|
| 2 |
+
file_extension: mp3
|
| 3 |
+
dataset_path: /bytecover_data/shs100k
|
| 4 |
+
save_model_path: models
|
| 5 |
+
debug: True
|
| 6 |
+
progress_bar: True
|
| 7 |
+
device: cpu
|
| 8 |
+
num_channels: 1
|
| 9 |
+
wandb: False
|
| 10 |
+
|
| 11 |
+
train:
|
| 12 |
+
mixed_precision: True
|
| 13 |
+
target_sr: 22050
|
| 14 |
+
compress_ratio: 20
|
| 15 |
+
max_seq_len: [100, 150, 200]
|
| 16 |
+
num_classes: 10000
|
| 17 |
+
triplet_margin: 0.3
|
| 18 |
+
smooth_factor: 0.1
|
| 19 |
+
model_ckpt: null
|
| 20 |
+
batch_size: 8
|
| 21 |
+
num_workers: 0
|
| 22 |
+
shuffle: True
|
| 23 |
+
drop_last: True
|
| 24 |
+
epochs: 1
|
| 25 |
+
learning_rate: 0.0001
|
| 26 |
+
patience: 4
|
| 27 |
+
tempo_factors: [0.7, 1.3]
|
| 28 |
+
log_steps: 20
|
| 29 |
+
|
| 30 |
+
val:
|
| 31 |
+
target_sr: 22050
|
| 32 |
+
compress_ratio: 20
|
| 33 |
+
save_val_outputs: True
|
| 34 |
+
max_seq_len: -1
|
| 35 |
+
batch_size: 1
|
| 36 |
+
num_workers: 8
|
| 37 |
+
shuffle: False
|
| 38 |
+
drop_last: False
|
| 39 |
+
output_dir: outputs_val
|
| 40 |
+
|
| 41 |
+
test:
|
| 42 |
+
target_sr: 22050
|
| 43 |
+
compress_ratio: 20
|
| 44 |
+
save_test_outputs: True
|
| 45 |
+
model_ckpt: orfium-bytecover.pt
|
| 46 |
+
max_seq_len: -1
|
| 47 |
+
batch_size: 1
|
| 48 |
+
num_workers: 8
|
| 49 |
+
shuffle: False
|
| 50 |
+
drop_last: False
|
| 51 |
+
output_dir: outputs_test
|
bytecover/config_gpu.yaml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data_path: bytecover_data
|
| 2 |
+
file_extension: mp3
|
| 3 |
+
dataset_path: /bytecover_data/shs100k
|
| 4 |
+
save_model_path: models
|
| 5 |
+
debug: True
|
| 6 |
+
progress_bar: True
|
| 7 |
+
device: cuda:0
|
| 8 |
+
num_channels: 1
|
| 9 |
+
wandb: False
|
| 10 |
+
|
| 11 |
+
train:
|
| 12 |
+
mixed_precision: True
|
| 13 |
+
target_sr: 22050
|
| 14 |
+
compress_ratio: 20
|
| 15 |
+
max_seq_len: [100, 150, 200]
|
| 16 |
+
num_classes: 10000
|
| 17 |
+
triplet_margin: 0.3
|
| 18 |
+
smooth_factor: 0.1
|
| 19 |
+
model_ckpt: null
|
| 20 |
+
batch_size: 8
|
| 21 |
+
num_workers: 0
|
| 22 |
+
shuffle: True
|
| 23 |
+
drop_last: True
|
| 24 |
+
epochs: 1
|
| 25 |
+
learning_rate: 0.0001
|
| 26 |
+
patience: 4
|
| 27 |
+
tempo_factors: [0.7, 1.3]
|
| 28 |
+
log_steps: 20
|
| 29 |
+
|
| 30 |
+
val:
|
| 31 |
+
target_sr: 22050
|
| 32 |
+
compress_ratio: 20
|
| 33 |
+
save_val_outputs: True
|
| 34 |
+
max_seq_len: -1
|
| 35 |
+
batch_size: 1
|
| 36 |
+
num_workers: 8
|
| 37 |
+
shuffle: False
|
| 38 |
+
drop_last: False
|
| 39 |
+
output_dir: outputs_val
|
| 40 |
+
|
| 41 |
+
test:
|
| 42 |
+
target_sr: 22050
|
| 43 |
+
compress_ratio: 20
|
| 44 |
+
save_test_outputs: True
|
| 45 |
+
model_ckpt: orfium-bytecover.pt
|
| 46 |
+
max_seq_len: -1
|
| 47 |
+
batch_size: 1
|
| 48 |
+
num_workers: 8
|
| 49 |
+
shuffle: False
|
| 50 |
+
drop_last: False
|
| 51 |
+
output_dir: outputs_test
|
bytecover/models/__init__.py
ADDED
|
File without changes
|
bytecover/models/data_loader.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Dict, Literal, Tuple
|
| 3 |
+
|
| 4 |
+
import ffmpeg
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from torch.utils.data import DataLoader, Dataset
|
| 10 |
+
from torchvision import transforms
|
| 11 |
+
|
| 12 |
+
from bytecover.models.data_model import BatchDict
|
| 13 |
+
from bytecover.utils import bcolors
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ByteCoverDataset(Dataset):
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
data_path: str,
|
| 20 |
+
file_ext: str,
|
| 21 |
+
dataset_path: str,
|
| 22 |
+
data_split: Literal["TRAIN", "VAL", "TEST"],
|
| 23 |
+
debug: bool,
|
| 24 |
+
target_sr: int,
|
| 25 |
+
max_len: int,
|
| 26 |
+
) -> None:
|
| 27 |
+
super().__init__()
|
| 28 |
+
self.data_path = data_path
|
| 29 |
+
self.file_ext = file_ext
|
| 30 |
+
self.dataset_path = dataset_path
|
| 31 |
+
self.data_split = data_split
|
| 32 |
+
self.debug = debug
|
| 33 |
+
self.target_sr = target_sr
|
| 34 |
+
self.max_len = max_len
|
| 35 |
+
self._load_data()
|
| 36 |
+
self.pipeline = transforms.Compose([self._read_audio, self._pad_or_trim_audio])
|
| 37 |
+
|
| 38 |
+
def __len__(self) -> int:
|
| 39 |
+
return len(self.track_ids)
|
| 40 |
+
|
| 41 |
+
def __getitem__(self, index: int) -> BatchDict:
|
| 42 |
+
track_id = self.track_ids[index]
|
| 43 |
+
anchor_audio = self.pipeline(track_id)
|
| 44 |
+
|
| 45 |
+
clique_id, pos_id, neg_id = self._triplet_sampling(track_id)
|
| 46 |
+
|
| 47 |
+
if self.data_split == "TRAIN":
|
| 48 |
+
positive_audio = self.pipeline(pos_id)
|
| 49 |
+
negative_audio = self.pipeline(neg_id)
|
| 50 |
+
else:
|
| 51 |
+
positive_audio = torch.empty(0)
|
| 52 |
+
negative_audio = torch.empty(0)
|
| 53 |
+
return dict(
|
| 54 |
+
anchor_id=track_id,
|
| 55 |
+
anchor=anchor_audio,
|
| 56 |
+
anchor_label=torch.tensor(clique_id, dtype=torch.float),
|
| 57 |
+
positive_id=pos_id,
|
| 58 |
+
positive=positive_audio,
|
| 59 |
+
negative_id=neg_id,
|
| 60 |
+
negative=negative_audio,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
def _triplet_sampling(self, track_id: str) -> Tuple[int, str, str]:
|
| 64 |
+
clique_id = self.labels.loc[track_id, "clique"]
|
| 65 |
+
versions = self.versions.loc[clique_id, "versions"]
|
| 66 |
+
np.random.shuffle(versions)
|
| 67 |
+
pos_list = np.setdiff1d(versions, track_id)
|
| 68 |
+
pos_id = np.random.choice(pos_list, 1)[0]
|
| 69 |
+
|
| 70 |
+
neg_id = self.labels[~self.labels.index.isin(versions)].sample(1).index[0]
|
| 71 |
+
|
| 72 |
+
return (clique_id, pos_id, neg_id)
|
| 73 |
+
|
| 74 |
+
def _load_data(self) -> None:
|
| 75 |
+
self.track_ids = np.load(
|
| 76 |
+
os.path.join(self.data_path, "splits", f"{self.data_split.lower()}_ids.npy"), allow_pickle=True
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
self.labels = pd.read_csv(os.path.join(self.data_path, "interim", "shs100k.csv"), usecols=["clique", "id"])
|
| 80 |
+
self.labels = self.labels[self.labels["id"].isin(self.track_ids)]
|
| 81 |
+
self.labels.dropna(inplace=True)
|
| 82 |
+
self.labels.set_index("id", inplace=True)
|
| 83 |
+
cliques = self.labels["clique"].unique()
|
| 84 |
+
mapping = {}
|
| 85 |
+
for k, clique in enumerate(cliques):
|
| 86 |
+
mapping[clique] = k
|
| 87 |
+
self.labels["clique"] = self.labels["clique"].map(lambda x: mapping[x])
|
| 88 |
+
|
| 89 |
+
self.versions = pd.read_csv(
|
| 90 |
+
os.path.join(self.data_path, "interim", "versions.csv"), converters={"versions": eval}
|
| 91 |
+
)
|
| 92 |
+
self.versions.dropna(inplace=True)
|
| 93 |
+
self.versions = self.versions[self.versions["clique"].isin(cliques)]
|
| 94 |
+
self.versions["clique"] = self.versions["clique"].map(lambda x: mapping[x])
|
| 95 |
+
self.versions.set_index("clique", inplace=True)
|
| 96 |
+
|
| 97 |
+
def _read_audio(self, track_id: str) -> torch.Tensor:
|
| 98 |
+
if self.debug:
|
| 99 |
+
seq_len = np.random.randint(10, 200) if self.max_len <= 0 else self.max_len
|
| 100 |
+
return torch.rand(seq_len * self.target_sr)
|
| 101 |
+
filename = os.path.join(self.dataset_path, f"{track_id}.{self.file_ext}")
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
| 105 |
+
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
|
| 106 |
+
out, _ = (
|
| 107 |
+
ffmpeg.input(filename, threads=0)
|
| 108 |
+
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=self.target_sr)
|
| 109 |
+
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
|
| 110 |
+
)
|
| 111 |
+
except ffmpeg.Error as e:
|
| 112 |
+
raise RuntimeError(
|
| 113 |
+
f"{bcolors.WARNING}Failed to load audio:{bcolors.FAIL + filename + bcolors.ENDC}\n{e.stderr.decode()}"
|
| 114 |
+
) from e
|
| 115 |
+
|
| 116 |
+
# int16 ranges between -2^15 and +2^15 (±32768). By convention, floating point audio data is
|
| 117 |
+
# normalized to the range of [-1.0, 1.0]
|
| 118 |
+
audio = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
| 119 |
+
|
| 120 |
+
return torch.from_numpy(audio)
|
| 121 |
+
|
| 122 |
+
def _pad_or_trim_audio(self, audio: torch.Tensor) -> torch.Tensor:
|
| 123 |
+
if self.max_len <= 0:
|
| 124 |
+
return audio
|
| 125 |
+
|
| 126 |
+
if (self.data_split == "TRAIN") and (audio.shape[-1] <= self.max_len * self.target_sr):
|
| 127 |
+
return F.pad(audio, (0, self.max_len * self.target_sr - audio.shape[-1]))
|
| 128 |
+
|
| 129 |
+
max_offset = audio.shape[-1] - self.max_len * self.target_sr
|
| 130 |
+
offset = np.random.randint(max_offset) if max_offset > 0 else 0
|
| 131 |
+
|
| 132 |
+
return audio[offset : (offset + self.max_len * self.target_sr)]
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def bytecover_dataloader(
|
| 136 |
+
data_path: str,
|
| 137 |
+
file_ext: str,
|
| 138 |
+
dataset_path: str,
|
| 139 |
+
data_split: Literal["TRAIN", "VAL", "TEST"],
|
| 140 |
+
debug: bool,
|
| 141 |
+
max_len: int,
|
| 142 |
+
batch_size: int,
|
| 143 |
+
target_sr: int,
|
| 144 |
+
**config: Dict,
|
| 145 |
+
) -> DataLoader:
|
| 146 |
+
return DataLoader(
|
| 147 |
+
ByteCoverDataset(data_path, file_ext, dataset_path, data_split, debug, target_sr=target_sr, max_len=max_len),
|
| 148 |
+
batch_size=batch_size if max_len > 0 else 1,
|
| 149 |
+
num_workers=config["num_workers"],
|
| 150 |
+
shuffle=config["shuffle"],
|
| 151 |
+
drop_last=config["drop_last"],
|
| 152 |
+
)
|
bytecover/models/data_model.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TypedDict
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ValDict(TypedDict):
|
| 7 |
+
anchor_id: str
|
| 8 |
+
positive_id: str
|
| 9 |
+
negative_id: str
|
| 10 |
+
f_t: torch.Tensor
|
| 11 |
+
f_c: torch.Tensor
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BatchDict(TypedDict):
|
| 15 |
+
anchor_id: str
|
| 16 |
+
anchor: torch.Tensor
|
| 17 |
+
anchor_label: torch.Tensor
|
| 18 |
+
positive_id: str
|
| 19 |
+
positive: torch.Tensor
|
| 20 |
+
negative_id: str
|
| 21 |
+
negative: torch.Tensor
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Postfix(TypedDict):
|
| 25 |
+
Epoch: int
|
| 26 |
+
train_loss: float
|
| 27 |
+
train_loss_step: float
|
| 28 |
+
train_cls_loss: float
|
| 29 |
+
train_cls_loss_step: float
|
| 30 |
+
train_triplet_loss: float
|
| 31 |
+
train_triplet_loss_step: float
|
| 32 |
+
val_loss: float
|
| 33 |
+
mr1: float
|
| 34 |
+
mAP: float
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class TestResults(TypedDict):
|
| 38 |
+
test_mr1: float
|
| 39 |
+
test_mAP: float
|
bytecover/models/early_stopper.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class EarlyStopper:
|
| 5 |
+
def __init__(self, patience: int = 1, min_delta: int = 0):
|
| 6 |
+
self.patience = patience
|
| 7 |
+
self.min_delta = min_delta
|
| 8 |
+
self.counter = 0
|
| 9 |
+
self.min_validation_loss = np.inf
|
| 10 |
+
|
| 11 |
+
def __call__(self, validation_loss) -> bool:
|
| 12 |
+
if validation_loss < self.min_validation_loss:
|
| 13 |
+
self.min_validation_loss = validation_loss
|
| 14 |
+
self.counter = 0
|
| 15 |
+
elif validation_loss >= (self.min_validation_loss + self.min_delta):
|
| 16 |
+
self.counter += 1
|
| 17 |
+
if self.counter >= self.patience:
|
| 18 |
+
return True
|
| 19 |
+
return False
|
bytecover/models/modules.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple
|
| 2 |
+
|
| 3 |
+
import nnAudio.features.cqt as nnAudio
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
import torchaudio.transforms as T
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class GeM(nn.Module):
|
| 11 |
+
def __init__(self, p=3, eps=1e-6):
|
| 12 |
+
super(GeM, self).__init__()
|
| 13 |
+
self.p = nn.Parameter(torch.ones(1) * p)
|
| 14 |
+
self.eps = eps
|
| 15 |
+
|
| 16 |
+
def forward(self, x):
|
| 17 |
+
return self.gem(x, p=self.p, eps=self.eps)
|
| 18 |
+
|
| 19 |
+
def gem(self, x, p=3, eps=1e-6):
|
| 20 |
+
return F.avg_pool2d(x.clamp(min=eps).pow(p), (x.size(-2), x.size(-1))).pow(1.0 / p)
|
| 21 |
+
|
| 22 |
+
def __repr__(self):
|
| 23 |
+
return f"{self.__class__.__name__}(p={self.p.data.tolist()[0]:.4f}, eps={str(self.eps)})"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class IBN(nn.Module):
|
| 27 |
+
r"""Instance-Batch Normalization layer from
|
| 28 |
+
`"Two at Once: Enhancing Learning and Generalization Capacities via IBN-Net"
|
| 29 |
+
<https://arxiv.org/pdf/1807.09441.pdf>`
|
| 30 |
+
Args:
|
| 31 |
+
planes (int): Number of channels for the input tensor
|
| 32 |
+
ratio (float): Ratio of instance normalization in the IBN layer
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, planes, ratio):
|
| 36 |
+
super(IBN, self).__init__()
|
| 37 |
+
self.half = int(planes * ratio)
|
| 38 |
+
self.IN = nn.InstanceNorm2d(self.half, affine=True)
|
| 39 |
+
self.BN = nn.BatchNorm2d(planes - self.half)
|
| 40 |
+
|
| 41 |
+
def forward(self, x):
|
| 42 |
+
split = torch.split(x, self.half, 1)
|
| 43 |
+
out1 = self.IN(split[0].contiguous())
|
| 44 |
+
out2 = self.BN(split[1].contiguous())
|
| 45 |
+
out = torch.cat((out1, out2), 1)
|
| 46 |
+
return out
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Bottleneck(nn.Module):
|
| 50 |
+
|
| 51 |
+
expansion: int = 4
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self, in_channels: int, out_channels: int, last: bool = False, downsample=None, stride=1, bias: bool = True
|
| 55 |
+
):
|
| 56 |
+
super(Bottleneck, self).__init__()
|
| 57 |
+
|
| 58 |
+
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=bias)
|
| 59 |
+
if not last:
|
| 60 |
+
# Apply Instance normalization in first half channels (ratio=0.5)
|
| 61 |
+
self.ibn = IBN(out_channels, ratio=0.5)
|
| 62 |
+
else:
|
| 63 |
+
self.ibn = nn.BatchNorm2d(out_channels)
|
| 64 |
+
|
| 65 |
+
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=bias)
|
| 66 |
+
self.batch_norm2 = nn.BatchNorm2d(out_channels)
|
| 67 |
+
|
| 68 |
+
self.conv3 = nn.Conv2d(
|
| 69 |
+
out_channels, out_channels * self.expansion, kernel_size=1, stride=1, padding=0, bias=bias
|
| 70 |
+
)
|
| 71 |
+
self.batch_norm3 = nn.BatchNorm2d(out_channels * self.expansion)
|
| 72 |
+
|
| 73 |
+
self.downsample = downsample
|
| 74 |
+
self.stride = stride
|
| 75 |
+
self.relu = nn.ReLU()
|
| 76 |
+
|
| 77 |
+
def forward(self, x: torch.Tensor):
|
| 78 |
+
residual = x.clone()
|
| 79 |
+
|
| 80 |
+
x = self.conv1(x)
|
| 81 |
+
x = self.ibn(x)
|
| 82 |
+
x = self.relu(x)
|
| 83 |
+
|
| 84 |
+
x = self.conv2(x)
|
| 85 |
+
x = self.batch_norm2(x)
|
| 86 |
+
x = self.relu(x)
|
| 87 |
+
|
| 88 |
+
x = self.conv3(x)
|
| 89 |
+
x = self.batch_norm3(x)
|
| 90 |
+
x = self.relu(x)
|
| 91 |
+
|
| 92 |
+
if self.downsample is not None:
|
| 93 |
+
residual = self.downsample(residual)
|
| 94 |
+
|
| 95 |
+
out = residual + x
|
| 96 |
+
out = self.relu(out)
|
| 97 |
+
|
| 98 |
+
return out
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class Resnet50(nn.Module):
|
| 102 |
+
def __init__(
|
| 103 |
+
self,
|
| 104 |
+
ResBlock: Bottleneck,
|
| 105 |
+
emb_dim: int = 2048,
|
| 106 |
+
num_channels: int = 1,
|
| 107 |
+
num_classes: int = 8858,
|
| 108 |
+
sr: int = 22050,
|
| 109 |
+
hop_lenght: int = 512,
|
| 110 |
+
n_bins=84,
|
| 111 |
+
bins_per_octave=12,
|
| 112 |
+
window="hann",
|
| 113 |
+
compress_ratio: int = 20,
|
| 114 |
+
tempo_factors: Tuple[float, float] = None,
|
| 115 |
+
) -> None:
|
| 116 |
+
|
| 117 |
+
super(Resnet50, self).__init__()
|
| 118 |
+
self.in_channels = 64
|
| 119 |
+
|
| 120 |
+
self.cqt = nnAudio.CQT2010v2(
|
| 121 |
+
sr=sr,
|
| 122 |
+
hop_length=hop_lenght,
|
| 123 |
+
n_bins=n_bins,
|
| 124 |
+
bins_per_octave=bins_per_octave,
|
| 125 |
+
window=window,
|
| 126 |
+
output_format="Complex",
|
| 127 |
+
verbose=False,
|
| 128 |
+
)
|
| 129 |
+
self.compress = nn.AvgPool2d((1, compress_ratio))
|
| 130 |
+
self.time_strech = T.TimeStretch(n_freq=n_bins)
|
| 131 |
+
self.tempo_factors = tempo_factors
|
| 132 |
+
|
| 133 |
+
self.conv1 = nn.Conv2d(
|
| 134 |
+
in_channels=num_channels, out_channels=64, kernel_size=7, stride=2, padding=3, bias=False
|
| 135 |
+
)
|
| 136 |
+
self.batch_norm1 = nn.BatchNorm2d(num_features=64)
|
| 137 |
+
self.relu = nn.ReLU()
|
| 138 |
+
self.max_pool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
|
| 139 |
+
|
| 140 |
+
self.layer1 = self._make_layer(ResBlock, blocks=3, planes=64, stride=1)
|
| 141 |
+
self.layer2 = self._make_layer(ResBlock, blocks=4, planes=128, stride=2)
|
| 142 |
+
self.layer3 = self._make_layer(ResBlock, blocks=6, planes=256, stride=2)
|
| 143 |
+
self.layer4 = self._make_layer(ResBlock, blocks=3, planes=512, stride=1, last=True)
|
| 144 |
+
|
| 145 |
+
self.gem_pool = GeM()
|
| 146 |
+
|
| 147 |
+
self.bn_fc = nn.BatchNorm1d(emb_dim)
|
| 148 |
+
self.fc = nn.Linear(emb_dim, num_classes, bias=False)
|
| 149 |
+
nn.init.kaiming_normal_(self.fc.weight)
|
| 150 |
+
|
| 151 |
+
def _make_layer(self, ResBlock: Bottleneck, blocks: int, planes: int, stride: int = 1, last: bool = False):
|
| 152 |
+
downsample = None
|
| 153 |
+
if stride != 1 or self.in_channels != planes * ResBlock.expansion:
|
| 154 |
+
downsample = nn.Sequential(
|
| 155 |
+
nn.Conv2d(self.in_channels, planes * ResBlock.expansion, kernel_size=1, stride=stride, bias=False),
|
| 156 |
+
nn.BatchNorm2d(planes * ResBlock.expansion),
|
| 157 |
+
)
|
| 158 |
+
layers = []
|
| 159 |
+
layers.append(
|
| 160 |
+
ResBlock(in_channels=self.in_channels, out_channels=planes, stride=stride, downsample=downsample, last=last)
|
| 161 |
+
)
|
| 162 |
+
self.in_channels = planes * ResBlock.expansion
|
| 163 |
+
for _ in range(1, blocks):
|
| 164 |
+
layers.append(ResBlock(in_channels=self.in_channels, out_channels=planes, last=last))
|
| 165 |
+
|
| 166 |
+
return nn.Sequential(*layers)
|
| 167 |
+
|
| 168 |
+
def forward(self, x: torch.Tensor):
|
| 169 |
+
|
| 170 |
+
x = self.cqt(x)
|
| 171 |
+
# Time-strech requires complex tensors, that's why cqt function returns complex
|
| 172 |
+
x = torch.view_as_complex(x)
|
| 173 |
+
if self.tempo_factors is not None:
|
| 174 |
+
rate = abs(self.tempo_factors[1] - self.tempo_factors[0]) * torch.rand(1).item() + min(self.tempo_factors)
|
| 175 |
+
strech = (
|
| 176 |
+
abs(1 - rate) > 7e-2
|
| 177 |
+
) # if the strech ratio is too close to 1 (i.e. 0.93 < ratio < 1.07), skip time streching
|
| 178 |
+
if self.training and strech:
|
| 179 |
+
x = self.time_strech(x, rate)
|
| 180 |
+
# Compress the magnitude of the CQT
|
| 181 |
+
x = self.compress(torch.abs(x))
|
| 182 |
+
|
| 183 |
+
# Unsqueeze to simulate 1-channel image
|
| 184 |
+
x = self.conv1(x.unsqueeze(1))
|
| 185 |
+
x = self.batch_norm1(x)
|
| 186 |
+
x = self.relu(x)
|
| 187 |
+
x = self.max_pool1(x)
|
| 188 |
+
|
| 189 |
+
x = self.layer1(x)
|
| 190 |
+
x = self.layer2(x)
|
| 191 |
+
x = self.layer3(x)
|
| 192 |
+
x = self.layer4(x)
|
| 193 |
+
|
| 194 |
+
f_t = self.gem_pool(x)
|
| 195 |
+
f_t = torch.flatten(f_t, start_dim=1)
|
| 196 |
+
|
| 197 |
+
f_c = self.bn_fc(f_t)
|
| 198 |
+
cls = self.fc(f_c)
|
| 199 |
+
|
| 200 |
+
return dict(f_t=f_t, f_c=f_c, cls=cls)
|
bytecover/models/train_module.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
from copy import deepcopy
|
| 4 |
+
from typing import Dict, List
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
import wandb
|
| 10 |
+
from tqdm import tqdm, trange
|
| 11 |
+
|
| 12 |
+
from bytecover.models.data_model import BatchDict, Postfix, TestResults, ValDict
|
| 13 |
+
from bytecover.models.early_stopper import EarlyStopper
|
| 14 |
+
from bytecover.models.modules import Bottleneck, Resnet50
|
| 15 |
+
from bytecover.models.utils import (
|
| 16 |
+
calculate_ranking_metrics,
|
| 17 |
+
dataloader_factory,
|
| 18 |
+
dir_checker,
|
| 19 |
+
save_best_log,
|
| 20 |
+
save_logs,
|
| 21 |
+
save_predictions,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
logger: logging.Logger = logging.getLogger() # The logger used to log output
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class TrainModule:
|
| 28 |
+
def __init__(self, config: Dict) -> None:
|
| 29 |
+
self.config = config
|
| 30 |
+
self.state = "initializing"
|
| 31 |
+
self.best_model_path: str = None
|
| 32 |
+
self.num_classes = self.config["train"]["num_classes"]
|
| 33 |
+
self.max_len = self.config["train"]["max_seq_len"][0]
|
| 34 |
+
|
| 35 |
+
self.model = Resnet50(
|
| 36 |
+
Bottleneck,
|
| 37 |
+
num_channels=self.config["num_channels"],
|
| 38 |
+
num_classes=self.num_classes,
|
| 39 |
+
compress_ratio=self.config["train"]["compress_ratio"],
|
| 40 |
+
tempo_factors=self.config["train"]["tempo_factors"],
|
| 41 |
+
)
|
| 42 |
+
self.model.to(self.config["device"])
|
| 43 |
+
if self.config["wandb"]:
|
| 44 |
+
wandb.watch(self.model)
|
| 45 |
+
self.postfix: Postfix = {}
|
| 46 |
+
|
| 47 |
+
self.triplet_loss = nn.TripletMarginLoss(margin=config["train"]["triplet_margin"])
|
| 48 |
+
self.cls_loss = nn.CrossEntropyLoss(label_smoothing=config["train"]["smooth_factor"])
|
| 49 |
+
|
| 50 |
+
self.early_stop = EarlyStopper(patience=self.config["train"]["patience"])
|
| 51 |
+
self.optimizer = self.configure_optimizers()
|
| 52 |
+
if self.config["device"] != "cpu":
|
| 53 |
+
self.scaler = torch.cuda.amp.GradScaler(enabled=self.config["train"]["mixed_precision"])
|
| 54 |
+
|
| 55 |
+
def pipeline(self) -> None:
|
| 56 |
+
self.config["val"]["output_dir"] = dir_checker(self.config["val"]["output_dir"])
|
| 57 |
+
|
| 58 |
+
if self.config["train"]["model_ckpt"] is not None:
|
| 59 |
+
self.model.load_state_dict(torch.load(self.config["train"]["model_ckpt"]), strict=False)
|
| 60 |
+
logger.info(f'Model loaded from checkpoint: {self.config["train"]["model_ckpt"]}')
|
| 61 |
+
|
| 62 |
+
self.t_loaders = dataloader_factory(config=self.config, data_split="TRAIN")
|
| 63 |
+
self.v_loader = dataloader_factory(config=self.config, data_split="VAL")[0]
|
| 64 |
+
|
| 65 |
+
self.state = "running"
|
| 66 |
+
|
| 67 |
+
self.pbar = trange(
|
| 68 |
+
self.config["train"]["epochs"], disable=(not self.config["progress_bar"]), position=0, leave=True
|
| 69 |
+
)
|
| 70 |
+
for epoch in self.pbar:
|
| 71 |
+
if self.state in ["early_stopped", "interrupted", "finished"]:
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
self.postfix["Epoch"] = epoch
|
| 75 |
+
self.pbar.set_postfix(self.postfix)
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
self.train_procedure()
|
| 79 |
+
except KeyboardInterrupt:
|
| 80 |
+
logger.warning("\nKeyboard Interrupt detected. Attempting gracefull shutdown...")
|
| 81 |
+
self.state = "interrupted"
|
| 82 |
+
except Exception as err:
|
| 83 |
+
raise (err)
|
| 84 |
+
|
| 85 |
+
if self.state == "interrupted":
|
| 86 |
+
self.validation_procedure()
|
| 87 |
+
self.pbar.set_postfix(
|
| 88 |
+
{k: self.postfix[k] for k in self.postfix.keys() & {"train_loss_step", "mr1", "mAP"}}
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
self.state = "finished"
|
| 92 |
+
|
| 93 |
+
def validate(self) -> None:
|
| 94 |
+
self.v_loader = dataloader_factory(config=self.config, data_split="VAL")[0]
|
| 95 |
+
self.state = "running"
|
| 96 |
+
self.validation_procedure()
|
| 97 |
+
self.state = "finished"
|
| 98 |
+
|
| 99 |
+
def test(self) -> None:
|
| 100 |
+
self.test_loader = dataloader_factory(config=self.config, data_split="TEST")[0]
|
| 101 |
+
self.test_results: TestResults = {}
|
| 102 |
+
|
| 103 |
+
if self.best_model_path is not None:
|
| 104 |
+
self.model.load_state_dict(torch.load(self.best_model_path), strict=False)
|
| 105 |
+
print(f"Best model loaded from checkpoint: {self.best_model_path}")
|
| 106 |
+
elif self.config["test"]["model_ckpt"] is not None:
|
| 107 |
+
self.model.load_state_dict(torch.load(self.config["test"]["model_ckpt"], map_location='cpu'), strict=False)
|
| 108 |
+
print(f'Model loaded from checkpoint: {self.config["test"]["model_ckpt"]}')
|
| 109 |
+
elif self.state == "initializing":
|
| 110 |
+
print("Warning: Testing with random weights")
|
| 111 |
+
|
| 112 |
+
self.state = "running"
|
| 113 |
+
self.test_procedure()
|
| 114 |
+
self.state = "finished"
|
| 115 |
+
|
| 116 |
+
def train_procedure(self) -> None:
|
| 117 |
+
self.model.train()
|
| 118 |
+
pbar_loaders = tqdm(self.t_loaders, disable=(not self.config["progress_bar"]), position=1, leave=False)
|
| 119 |
+
for _, t_loader in enumerate(pbar_loaders):
|
| 120 |
+
train_loss_list = []
|
| 121 |
+
train_cls_loss_list = []
|
| 122 |
+
train_triplet_loss_list = []
|
| 123 |
+
self.max_len = t_loader.dataset.max_len
|
| 124 |
+
pbar_loaders.set_postfix_str(f"max_seq_len={self.max_len}")
|
| 125 |
+
for step, batch in tqdm(
|
| 126 |
+
enumerate(t_loader),
|
| 127 |
+
total=len(t_loader),
|
| 128 |
+
disable=(not self.config["progress_bar"]),
|
| 129 |
+
position=2,
|
| 130 |
+
leave=False,
|
| 131 |
+
):
|
| 132 |
+
train_step = self.training_step(batch)
|
| 133 |
+
self.postfix["train_loss_step"] = float(f"{train_step['train_loss_step']:.3f}")
|
| 134 |
+
train_loss_list.append(train_step["train_loss_step"])
|
| 135 |
+
self.postfix["train_cls_loss_step"] = float(f"{train_step['train_cls_loss']:.3f}")
|
| 136 |
+
train_cls_loss_list.append(train_step["train_cls_loss"])
|
| 137 |
+
self.postfix["train_triplet_loss_step"] = float(f"{train_step['train_triplet_loss']:.3f}")
|
| 138 |
+
train_triplet_loss_list.append(train_step["train_triplet_loss"])
|
| 139 |
+
self.pbar.set_postfix(
|
| 140 |
+
{k: self.postfix[k] for k in self.postfix.keys() & {"train_loss_step", "mr1", "mAP"}}
|
| 141 |
+
)
|
| 142 |
+
if self.config["wandb"]:
|
| 143 |
+
wandb.log(self.postfix)
|
| 144 |
+
if step % self.config["train"]["log_steps"] == 0:
|
| 145 |
+
save_logs(
|
| 146 |
+
dict(
|
| 147 |
+
epoch=self.postfix["Epoch"],
|
| 148 |
+
seq_len=self.max_len,
|
| 149 |
+
step=step,
|
| 150 |
+
train_loss_step=f"{train_step['train_loss_step']:.3f}",
|
| 151 |
+
train_cls_loss_step=f"{train_step['train_cls_loss']:.3f}",
|
| 152 |
+
train_triplet_loss_step=f"{train_step['train_triplet_loss']:.3f}",
|
| 153 |
+
),
|
| 154 |
+
output_dir=self.config["val"]["output_dir"],
|
| 155 |
+
name="log_steps",
|
| 156 |
+
)
|
| 157 |
+
train_loss = torch.tensor(train_loss_list)
|
| 158 |
+
train_cls_loss = torch.tensor(train_cls_loss_list)
|
| 159 |
+
train_triplet_loss = torch.tensor(train_triplet_loss_list)
|
| 160 |
+
self.postfix["train_loss"] = train_loss.mean().item()
|
| 161 |
+
self.postfix["train_cls_loss"] = train_cls_loss.mean().item()
|
| 162 |
+
self.postfix["train_triplet_loss"] = train_triplet_loss.mean().item()
|
| 163 |
+
if self.config["wandb"]:
|
| 164 |
+
wandb.log(self.postfix)
|
| 165 |
+
self.validation_procedure()
|
| 166 |
+
if self.config["wandb"]:
|
| 167 |
+
wandb.log(self.postfix)
|
| 168 |
+
self.overfit_check()
|
| 169 |
+
self.pbar.set_postfix({k: self.postfix[k] for k in self.postfix.keys() & {"train_loss_step", "mr1", "mAP"}})
|
| 170 |
+
|
| 171 |
+
def training_step(self, batch: BatchDict) -> Dict[str, float]:
|
| 172 |
+
with torch.autocast(
|
| 173 |
+
device_type=self.config["device"].split(":")[0], enabled=self.config["train"]["mixed_precision"]
|
| 174 |
+
):
|
| 175 |
+
anchor = self.model.forward(batch["anchor"].to(self.config["device"]))
|
| 176 |
+
positive = self.model.forward(batch["positive"].to(self.config["device"]))
|
| 177 |
+
negative = self.model.forward(batch["negative"].to(self.config["device"]))
|
| 178 |
+
l1 = self.triplet_loss(anchor["f_t"], positive["f_t"], negative["f_t"])
|
| 179 |
+
labels = nn.functional.one_hot(batch["anchor_label"].long(), num_classes=self.num_classes)
|
| 180 |
+
l2 = self.cls_loss(anchor["cls"], labels.float().to(self.config["device"]))
|
| 181 |
+
loss = l1 + l2
|
| 182 |
+
|
| 183 |
+
self.optimizer.zero_grad()
|
| 184 |
+
if self.config["device"] != "cpu":
|
| 185 |
+
self.scaler.scale(loss).backward()
|
| 186 |
+
self.scaler.step(self.optimizer)
|
| 187 |
+
self.scaler.update()
|
| 188 |
+
else:
|
| 189 |
+
loss.backward()
|
| 190 |
+
self.optimizer.step()
|
| 191 |
+
|
| 192 |
+
return {"train_loss_step": loss.item(), "train_triplet_loss": l1.item(), "train_cls_loss": l2.item()}
|
| 193 |
+
|
| 194 |
+
def validation_procedure(self) -> None:
|
| 195 |
+
self.model.eval()
|
| 196 |
+
embeddings: Dict[str, torch.Tensor] = {}
|
| 197 |
+
for batch in tqdm(self.v_loader, disable=(not self.config["progress_bar"]), position=1, leave=False):
|
| 198 |
+
val_dict = self.validation_step(batch)
|
| 199 |
+
if val_dict["f_t"].ndim == 1:
|
| 200 |
+
val_dict["f_c"] = val_dict["f_c"].unsqueeze(0)
|
| 201 |
+
val_dict["f_t"] = val_dict["f_t"].unsqueeze(0)
|
| 202 |
+
for anchor_id, triplet_embedding, embedding in zip(val_dict["anchor_id"], val_dict["f_t"], val_dict["f_c"]):
|
| 203 |
+
embeddings[anchor_id] = torch.stack([triplet_embedding, embedding])
|
| 204 |
+
|
| 205 |
+
val_outputs = self.validation_epoch_end(embeddings)
|
| 206 |
+
logger.info(
|
| 207 |
+
f"\n{' Validation Results ':=^50}\n"
|
| 208 |
+
+ "\n".join([f'"{key}": {value}' for key, value in self.postfix.items()])
|
| 209 |
+
+ f"\n{' End of Validation ':=^50}\n"
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
if self.config["val"]["save_val_outputs"]:
|
| 213 |
+
val_outputs["val_embeddings"] = torch.stack(list(embeddings.values()))[:, 1].numpy()
|
| 214 |
+
save_predictions(val_outputs, output_dir=self.config["val"]["output_dir"])
|
| 215 |
+
save_logs(self.postfix, output_dir=self.config["val"]["output_dir"])
|
| 216 |
+
self.model.train()
|
| 217 |
+
|
| 218 |
+
def validation_epoch_end(self, outputs: Dict[str, torch.Tensor]) -> Dict[str, np.ndarray]:
|
| 219 |
+
val_loss = torch.zeros(len(outputs))
|
| 220 |
+
pos_ids = []
|
| 221 |
+
neg_ids = []
|
| 222 |
+
clique_ids = []
|
| 223 |
+
for k, (anchor_id, embeddings) in enumerate(outputs.items()):
|
| 224 |
+
clique_id, pos_id, neg_id = self.v_loader.dataset._triplet_sampling(anchor_id)
|
| 225 |
+
val_loss[k] = self.triplet_loss(embeddings[0], outputs[pos_id][0], outputs[neg_id][0]).item()
|
| 226 |
+
pos_ids.append(pos_id)
|
| 227 |
+
neg_ids.append(neg_id)
|
| 228 |
+
clique_ids.append(clique_id)
|
| 229 |
+
anchor_ids = np.stack(list(outputs.keys()))
|
| 230 |
+
preds = torch.stack(list(outputs.values()))[:, 1]
|
| 231 |
+
self.postfix["val_loss"] = val_loss.mean().item()
|
| 232 |
+
ranks, average_precisions = calculate_ranking_metrics(embeddings=preds.numpy(), cliques=clique_ids)
|
| 233 |
+
self.postfix["mr1"] = ranks.mean()
|
| 234 |
+
self.postfix["mAP"] = average_precisions.mean()
|
| 235 |
+
return {
|
| 236 |
+
"triplet_ids": np.stack(list(zip(clique_ids, anchor_ids, pos_ids, neg_ids))),
|
| 237 |
+
"ranks": ranks,
|
| 238 |
+
"average_precisions": average_precisions,
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
def validation_step(self, batch: BatchDict) -> ValDict:
|
| 242 |
+
anchor_id = batch["anchor_id"]
|
| 243 |
+
positive_id = batch["positive_id"]
|
| 244 |
+
negative_id = batch["negative_id"]
|
| 245 |
+
|
| 246 |
+
features = self.model.forward(batch["anchor"].to(self.config["device"]))
|
| 247 |
+
|
| 248 |
+
return {
|
| 249 |
+
"anchor_id": anchor_id,
|
| 250 |
+
"positive_id": positive_id,
|
| 251 |
+
"negative_id": negative_id,
|
| 252 |
+
"f_t": features["f_t"].squeeze(0).detach().cpu(),
|
| 253 |
+
"f_c": features["f_c"].squeeze(0).detach().cpu(),
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
def test_procedure(self) -> None:
|
| 257 |
+
self.model.eval()
|
| 258 |
+
clique_ids = []
|
| 259 |
+
embeddings: Dict[str, torch.Tensor] = {}
|
| 260 |
+
for batch in tqdm(self.test_loader, disable=(not self.config["progress_bar"])):
|
| 261 |
+
clique_ids_batch = self.test_loader.dataset.labels.loc[batch["anchor_id"], "clique"]
|
| 262 |
+
test_dict = self.validation_step(batch)
|
| 263 |
+
if test_dict["f_c"].ndim == 1:
|
| 264 |
+
test_dict["f_c"] = test_dict["f_c"].unsqueeze(0)
|
| 265 |
+
for anchor_id, clique_id, embedding in zip(test_dict["anchor_id"], clique_ids_batch, test_dict["f_c"]):
|
| 266 |
+
embeddings[anchor_id] = embedding
|
| 267 |
+
clique_ids.append(clique_id)
|
| 268 |
+
|
| 269 |
+
test_outputs = self.test_epoch_end(embeddings, clique_ids)
|
| 270 |
+
logger.info(
|
| 271 |
+
f"\n{' Test Results ':=^50}\n"
|
| 272 |
+
+ "\n".join([f'"{key}": {value}' for key, value in self.test_results.items()])
|
| 273 |
+
+ f"\n{' End of Testing ':=^50}\n"
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
if self.config["test"]["save_test_outputs"]:
|
| 277 |
+
test_outputs["test_embeddings"] = torch.stack(list(embeddings.values())).numpy()
|
| 278 |
+
save_predictions(test_outputs, output_dir=self.config["test"]["output_dir"])
|
| 279 |
+
save_logs(self.test_results, output_dir=self.config["test"]["output_dir"])
|
| 280 |
+
|
| 281 |
+
def test_epoch_end(self, outputs: Dict[str, torch.Tensor], clique_ids: List[int]) -> Dict[str, np.ndarray]:
|
| 282 |
+
anchor_ids = np.stack(list(outputs.keys()))
|
| 283 |
+
preds = torch.stack(list(outputs.values()))
|
| 284 |
+
ranks, average_precisions = calculate_ranking_metrics(embeddings=preds.numpy(), cliques=clique_ids)
|
| 285 |
+
self.test_results["test_mr1"] = ranks.mean()
|
| 286 |
+
self.test_results["test_mAP"] = average_precisions.mean()
|
| 287 |
+
return {
|
| 288 |
+
"anchor_ids": np.stack(list(zip(clique_ids, anchor_ids))),
|
| 289 |
+
"ranks": ranks,
|
| 290 |
+
"average_precisions": average_precisions,
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
def overfit_check(self) -> None:
|
| 294 |
+
if self.early_stop(self.postfix["val_loss"]):
|
| 295 |
+
logger.info(f"\nValidation not improved for {self.early_stop.patience} consecutive epochs. Stopping...")
|
| 296 |
+
self.state = "early_stopped"
|
| 297 |
+
|
| 298 |
+
if self.early_stop.counter > 0:
|
| 299 |
+
logger.info("\nValidation loss was not improved")
|
| 300 |
+
else:
|
| 301 |
+
logger.info(f"\nMetric improved. New best score: {self.early_stop.min_validation_loss:.3f}")
|
| 302 |
+
save_best_log(self.postfix, output_dir=self.config["val"]["output_dir"])
|
| 303 |
+
|
| 304 |
+
logger.info("Saving model...")
|
| 305 |
+
epoch = self.postfix["Epoch"]
|
| 306 |
+
max_secs = self.max_len
|
| 307 |
+
prev_model = deepcopy(self.best_model_path)
|
| 308 |
+
self.best_model_path = os.path.join(
|
| 309 |
+
self.config["val"]["output_dir"], "model", f"best-model-{epoch=}-{max_secs=}.pt"
|
| 310 |
+
)
|
| 311 |
+
os.makedirs(os.path.dirname(self.best_model_path), exist_ok=True)
|
| 312 |
+
torch.save(deepcopy(self.model.state_dict()), self.best_model_path)
|
| 313 |
+
if prev_model is not None:
|
| 314 |
+
os.remove(prev_model)
|
| 315 |
+
|
| 316 |
+
def configure_optimizers(self) -> torch.optim.Optimizer:
|
| 317 |
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=self.config["train"]["learning_rate"])
|
| 318 |
+
|
| 319 |
+
return optimizer
|
bytecover/models/utils.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import glob
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
from typing import Dict, List, Tuple
|
| 6 |
+
|
| 7 |
+
import jsonlines
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
from sklearn.metrics import pairwise_distances
|
| 11 |
+
from torch.utils.data import DataLoader
|
| 12 |
+
|
| 13 |
+
from bytecover.models.data_loader import bytecover_dataloader
|
| 14 |
+
from bytecover.models.data_model import Postfix
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def dataloader_factory(config: Dict, data_split: str) -> List[DataLoader]:
|
| 18 |
+
seq_len_key = "max_seq_len"
|
| 19 |
+
|
| 20 |
+
if data_split == "TRAIN":
|
| 21 |
+
t_loaders = []
|
| 22 |
+
for L in config["train"][seq_len_key]:
|
| 23 |
+
t_loaders.append(
|
| 24 |
+
bytecover_dataloader(
|
| 25 |
+
data_path=config["data_path"],
|
| 26 |
+
file_ext=config["file_extension"],
|
| 27 |
+
dataset_path=config["dataset_path"],
|
| 28 |
+
data_split=data_split,
|
| 29 |
+
debug=config["debug"],
|
| 30 |
+
max_len=L,
|
| 31 |
+
**config["train"],
|
| 32 |
+
)
|
| 33 |
+
)
|
| 34 |
+
return t_loaders
|
| 35 |
+
L = config[data_split.lower()][seq_len_key]
|
| 36 |
+
return [
|
| 37 |
+
bytecover_dataloader(
|
| 38 |
+
data_path=config["data_path"],
|
| 39 |
+
file_ext=config["file_extension"],
|
| 40 |
+
dataset_path=config["dataset_path"],
|
| 41 |
+
data_split=data_split,
|
| 42 |
+
debug=config["debug"],
|
| 43 |
+
max_len=L,
|
| 44 |
+
**config[data_split.lower()],
|
| 45 |
+
)
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def validation_triplet_sampling(anchor_id: str, val_ids: List[str], df: pd.DataFrame) -> Dict[str, int]:
|
| 50 |
+
np.random.shuffle(df.loc[anchor_id, "versions"])
|
| 51 |
+
pos_list = np.setdiff1d(df.loc[anchor_id, "versions"], anchor_id)
|
| 52 |
+
pos_id = np.random.choice(pos_list, 1)[0]
|
| 53 |
+
pos_id = val_ids.index(pos_id)
|
| 54 |
+
|
| 55 |
+
neg_id = df.loc[~df.index.isin([anchor_id] + list(pos_list))].sample(1).index[0]
|
| 56 |
+
neg_id = val_ids.index(neg_id)
|
| 57 |
+
|
| 58 |
+
return dict(pos_id=pos_id, neg_id=neg_id)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def calculate_ranking_metrics(embeddings: np.ndarray, cliques: List[int]) -> Tuple[np.ndarray, np.ndarray]:
|
| 62 |
+
distances = pairwise_distances(embeddings)
|
| 63 |
+
s_distances = np.argsort(distances, axis=1)
|
| 64 |
+
cliques = np.array(cliques)
|
| 65 |
+
query_cliques = cliques[s_distances[:, 0]]
|
| 66 |
+
search_cliques = cliques[s_distances[:, 1:]]
|
| 67 |
+
|
| 68 |
+
query_cliques = np.tile(query_cliques, (search_cliques.shape[-1], 1)).T
|
| 69 |
+
mask = np.equal(search_cliques, query_cliques)
|
| 70 |
+
|
| 71 |
+
ranks = mask.argmax(axis=1)
|
| 72 |
+
|
| 73 |
+
cumsum = np.cumsum(mask, axis=1)
|
| 74 |
+
mask2 = mask * cumsum
|
| 75 |
+
mask2 = mask2 / np.arange(1, mask2.shape[-1] + 1)
|
| 76 |
+
average_precisions = np.sum(mask2, axis=1) / np.sum(mask, axis=1)
|
| 77 |
+
|
| 78 |
+
return (ranks, average_precisions)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def dir_checker(output_dir: str) -> str:
|
| 82 |
+
output_dir = re.sub(r"run-[0-9]+/*", "", output_dir)
|
| 83 |
+
runs = glob.glob(os.path.join(output_dir, "run-*"))
|
| 84 |
+
if runs != []:
|
| 85 |
+
max_run = max(map(lambda x: int(x.split("-")[-1]), runs))
|
| 86 |
+
run = max_run + 1
|
| 87 |
+
else:
|
| 88 |
+
run = 0
|
| 89 |
+
outdir = os.path.join(output_dir, f"run-{run}")
|
| 90 |
+
return outdir
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def save_predictions(outputs: Dict[str, np.ndarray], output_dir: str) -> None:
|
| 94 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 95 |
+
for key in outputs:
|
| 96 |
+
if "_ids" in key:
|
| 97 |
+
with jsonlines.open(os.path.join(output_dir, f"{key}.jsonl"), "w") as f:
|
| 98 |
+
if len(outputs[key][0]) == 4:
|
| 99 |
+
for clique, anchor, pos, neg in outputs[key]:
|
| 100 |
+
f.write({"clique_id": clique, "anchor_id": anchor, "positive_id": pos, "negative_id": neg})
|
| 101 |
+
else:
|
| 102 |
+
for clique, anchor in outputs[key]:
|
| 103 |
+
f.write({"clique_id": clique, "anchor_id": anchor})
|
| 104 |
+
else:
|
| 105 |
+
np.save(os.path.join(output_dir, f"{key}.npy"), outputs[key])
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def save_logs(outputs: dict, output_dir: str, name: str = "log") -> None:
|
| 109 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 110 |
+
log_file = os.path.join(output_dir, f"{name}.jsonl")
|
| 111 |
+
with jsonlines.open(log_file, "a") as f:
|
| 112 |
+
f.write(outputs)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def save_best_log(outputs: Postfix, output_dir: str) -> None:
|
| 116 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 117 |
+
log_file = os.path.join(output_dir, "best-log.json")
|
| 118 |
+
with open(log_file, "w") as f:
|
| 119 |
+
json.dump(outputs, f, indent=2)
|
bytecover/utils.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from logging import config as logging_config
|
| 3 |
+
from typing import Dict
|
| 4 |
+
|
| 5 |
+
from yaml import FullLoader, load, safe_load
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class bcolors:
|
| 10 |
+
OKGREEN: str = "\033[92m"
|
| 11 |
+
WARNING: str = "\033[93m"
|
| 12 |
+
FAIL: str = "\033[91m"
|
| 13 |
+
ENDC: str = "\033[0m"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_config(config_path: str) -> Dict:
|
| 17 |
+
with open(config_path) as file:
|
| 18 |
+
config = safe_load(file)
|
| 19 |
+
|
| 20 |
+
if config["device"] == "gpu":
|
| 21 |
+
config["device"] = "cuda:0"
|
| 22 |
+
|
| 23 |
+
return config
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def initialize_logging(config_path: str, debug: bool) -> None:
|
| 27 |
+
"""
|
| 28 |
+
Setup logging according to the configuration in the given file.
|
| 29 |
+
:param str config_path: The path to the file containing the logging configuration
|
| 30 |
+
:return:
|
| 31 |
+
"""
|
| 32 |
+
with open(config_path) as yaml_fh:
|
| 33 |
+
config_description = load(yaml_fh, Loader=FullLoader)
|
| 34 |
+
if debug:
|
| 35 |
+
config_description["root"]["level"] = "DEBUG"
|
| 36 |
+
logging_config.dictConfig(config_description)
|
orfium-bytecover.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6fb5eb4c20ab9a7aea7fadf4893b5c1c778281caffb76af143b7a4798c3225eb
|
| 3 |
+
size 176417247
|
pinecone_generate.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generate ByteCover and CLAP Embeddings for a dataset and put to Pinecone
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
from typing import Iterator
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import torch
|
| 11 |
+
import laion_clap
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
from pinecone.grpc import PineconeGRPC as Pinecone
|
| 14 |
+
from pinecone import PodSpec, PineconeApiException
|
| 15 |
+
import ffmpeg
|
| 16 |
+
|
| 17 |
+
from bytecover.models.train_module import TrainModule
|
| 18 |
+
from bytecover.models.data_loader import ByteCoverDataset
|
| 19 |
+
from bytecover.utils import load_config
|
| 20 |
+
|
| 21 |
+
class BatchGenerator:
|
| 22 |
+
#
|
| 23 |
+
def __init__(self, batch_size: int = 10) -> None:
|
| 24 |
+
self.batch_size = batch_size
|
| 25 |
+
#
|
| 26 |
+
# Makes chunks out of an input DataFrame
|
| 27 |
+
def to_batches(self, df: pd.DataFrame) -> Iterator[pd.DataFrame]:
|
| 28 |
+
splits = self.splits_num(df.shape[0])
|
| 29 |
+
if splits <= 1:
|
| 30 |
+
yield df
|
| 31 |
+
else:
|
| 32 |
+
for chunk in np.array_split(df, splits):
|
| 33 |
+
yield chunk
|
| 34 |
+
#
|
| 35 |
+
# Determines how many chunks DataFrame contains
|
| 36 |
+
def splits_num(self, elements: int) -> int:
|
| 37 |
+
return round(elements / self.batch_size)
|
| 38 |
+
#
|
| 39 |
+
__call__ = to_batches
|
| 40 |
+
|
| 41 |
+
# quantization
|
| 42 |
+
def int16_to_float32(x):
|
| 43 |
+
return (x / 32767.0).astype(np.float32)
|
| 44 |
+
|
| 45 |
+
def float32_to_int16(x):
|
| 46 |
+
x = np.clip(x, a_min=-1., a_max=1.)
|
| 47 |
+
return (x * 32767.).astype(np.int16)
|
| 48 |
+
|
| 49 |
+
def flatten_vector_embed(vector_embed):
|
| 50 |
+
return list(vector_embed.flatten())
|
| 51 |
+
|
| 52 |
+
def grab_song_title(vector_name):
|
| 53 |
+
return vector_name.split("_")[0]
|
| 54 |
+
|
| 55 |
+
def convert_to_npfloat64(original_array):
|
| 56 |
+
#return np.array(flat_df["flat_vector_embed"][0],dtype=np.float64)
|
| 57 |
+
return np.array(original_array,dtype=np.float64)
|
| 58 |
+
|
| 59 |
+
def convert_to_npfloat64_to_list(vector_embed_64):
|
| 60 |
+
# list(flat_df["flat_vector_embed_64"][0])
|
| 61 |
+
return list(vector_embed_64)
|
| 62 |
+
|
| 63 |
+
def look_up_metadata(track_id, meta_dataframe, meta_col_interest):
|
| 64 |
+
# track_id: form = spotify:track:id_##,mp3
|
| 65 |
+
# meta_datframe: df of all the metavalues
|
| 66 |
+
# column options = album, artist_names, popularity, release_date, genre
|
| 67 |
+
df_id = track_id.split("_")[0]
|
| 68 |
+
meta_row = meta_dataframe[meta_dataframe['uri'] == df_id].reset_index(drop=True)
|
| 69 |
+
try:
|
| 70 |
+
return meta_row[meta_col_interest][0]
|
| 71 |
+
except:
|
| 72 |
+
return "unknown"
|
| 73 |
+
#return meta_row[meta_col_interest][0]
|
| 74 |
+
|
| 75 |
+
def strip_year_from_date(full_date):
|
| 76 |
+
if type(full_date) == int:
|
| 77 |
+
return str(full_date)
|
| 78 |
+
else:
|
| 79 |
+
try:
|
| 80 |
+
return full_date[:4]
|
| 81 |
+
except:
|
| 82 |
+
return "CHECK_THIS"
|
| 83 |
+
|
| 84 |
+
def strip_vector_clip(vector_name):
|
| 85 |
+
return vector_name.split(".")[0].split("_")[1]
|
| 86 |
+
|
| 87 |
+
def get_triplet_num(vector_name_str):
|
| 88 |
+
return str(int(vector_name_str.split("_")[2].split(".")[0]) + 1)
|
| 89 |
+
|
| 90 |
+
def generate(audio_dir, metadata_dir, index_naming_conv):
|
| 91 |
+
|
| 92 |
+
# FILE AND METADATA LOADING
|
| 93 |
+
|
| 94 |
+
file_list = [f for f in os.listdir(audio_dir)]
|
| 95 |
+
print(f"Found {len(file_list)} files")
|
| 96 |
+
|
| 97 |
+
meta_list = [f for f in os.listdir(metadata_dir)]
|
| 98 |
+
meta_list = sorted(meta_list)
|
| 99 |
+
|
| 100 |
+
meta_df = pd.read_json(metadata_dir + "/" + meta_list[0])
|
| 101 |
+
|
| 102 |
+
for i in range(1, len(meta_list)-1):
|
| 103 |
+
new_row = pd.read_json(metadata_dir + "/" + meta_list[i])
|
| 104 |
+
meta_df = pd.concat([meta_df, new_row]).reset_index(drop = True)
|
| 105 |
+
|
| 106 |
+
meta_df["year"] = meta_df.apply(lambda row: strip_year_from_date(row['release_date']),axis=1)
|
| 107 |
+
|
| 108 |
+
# BYTECOVER MODEL INITIALIZATION
|
| 109 |
+
|
| 110 |
+
print("Loading ByteCover model")
|
| 111 |
+
|
| 112 |
+
bytecover_config = load_config(config_path="bytecover/config.yaml")
|
| 113 |
+
bytecover_module = TrainModule(bytecover_config)
|
| 114 |
+
bytecover_model = bytecover_module.model
|
| 115 |
+
if bytecover_module.best_model_path is not None:
|
| 116 |
+
bytecover_model.load_state_dict(torch.load(bytecover_module.best_model_path), strict=False)
|
| 117 |
+
print(f"Best model loaded from checkpoint: {bytecover_module.best_model_path}")
|
| 118 |
+
elif bytecover_module.config["test"]["model_ckpt"] is not None:
|
| 119 |
+
bytecover_model.load_state_dict(torch.load(bytecover_module.config["test"]["model_ckpt"], map_location='cpu'), strict=False)
|
| 120 |
+
print(f'Model loaded from checkpoint: {bytecover_module.config["test"]["model_ckpt"]}')
|
| 121 |
+
elif bytecover_module.state == "initializing":
|
| 122 |
+
print("Warning: Running with random weights")
|
| 123 |
+
|
| 124 |
+
bytecover_model.eval()
|
| 125 |
+
|
| 126 |
+
# BYTECOVER EMBEDDING GENERATION
|
| 127 |
+
|
| 128 |
+
audio_dict_bytecover = {}
|
| 129 |
+
for file in tqdm(file_list, desc="Generating Bytecover Embeddings"):
|
| 130 |
+
file_path = audio_dir + file
|
| 131 |
+
# try statement here allows you to skip to items you haven't yet embedded if you stop this step midway (if a key exists, you move on to next key)
|
| 132 |
+
try:
|
| 133 |
+
audio_dict_bytecover[file]
|
| 134 |
+
except:
|
| 135 |
+
# Load audio
|
| 136 |
+
try:
|
| 137 |
+
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
| 138 |
+
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
|
| 139 |
+
out, _ = (
|
| 140 |
+
ffmpeg.input(file_path, threads=0)
|
| 141 |
+
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=22050)
|
| 142 |
+
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
|
| 143 |
+
)
|
| 144 |
+
except ffmpeg.Error as e:
|
| 145 |
+
raise RuntimeError(
|
| 146 |
+
f"Failed to load audio:{file_path}\n{e.stderr.decode()}"
|
| 147 |
+
) from e
|
| 148 |
+
audio = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
| 149 |
+
song_tensor = torch.from_numpy(audio)
|
| 150 |
+
# this step grabs a ByteCover embedding
|
| 151 |
+
audio_embed = bytecover_model.forward(song_tensor.to(bytecover_module.config["device"]))['f_t'].detach()
|
| 152 |
+
audio_dict_bytecover[file] = audio_embed.squeeze()
|
| 153 |
+
|
| 154 |
+
# CLAP MODEL INITIALIZATION
|
| 155 |
+
|
| 156 |
+
print("Loading CLAP model")
|
| 157 |
+
|
| 158 |
+
clap_model = laion_clap.CLAP_Module(enable_fusion=False)
|
| 159 |
+
clap_model.load_ckpt() # download the default pretrained checkpoint.
|
| 160 |
+
|
| 161 |
+
# CLAP EMBEDDING GENERATION
|
| 162 |
+
|
| 163 |
+
audio_dict_CLAP = {}
|
| 164 |
+
for file in tqdm(file_list, desc="Generating CLAP Embeddings"):
|
| 165 |
+
# try statement here allows you to skip to items you haven't yet embedded if you stop this step midway (if a key exists, you move on to next key)
|
| 166 |
+
try:
|
| 167 |
+
audio_dict_CLAP[file]
|
| 168 |
+
except:
|
| 169 |
+
# Get audio embeddings from audio data
|
| 170 |
+
full_path = audio_dir + "/" + file
|
| 171 |
+
# this step grabs a CLAP embedding from laion_clap library
|
| 172 |
+
audio_embed = clap_model.get_audio_embedding_from_filelist(x = [full_path], use_tensor=False)
|
| 173 |
+
audio_dict_CLAP[file] = audio_embed
|
| 174 |
+
|
| 175 |
+
# DATAFRAME GENERATION
|
| 176 |
+
|
| 177 |
+
flat_dfs = []
|
| 178 |
+
|
| 179 |
+
for audio_dict in [audio_dict_CLAP, audio_dict_bytecover]:
|
| 180 |
+
|
| 181 |
+
flat_df = pd.DataFrame(audio_dict.items(), columns=['vector_name','vector_embed']).reset_index()
|
| 182 |
+
flat_df.columns=['vector_id','vector_name','vector_embed']
|
| 183 |
+
|
| 184 |
+
flat_df["song_title"] = flat_df.apply(lambda row: grab_song_title(row['vector_name']),axis=1)
|
| 185 |
+
flat_df["flat_vector_embed"] = flat_df.apply(lambda row: flatten_vector_embed(row['vector_embed']),axis=1)
|
| 186 |
+
|
| 187 |
+
flat_df["flat_vector_embed_64"] = flat_df.apply(lambda row: convert_to_npfloat64(row['flat_vector_embed']),axis=1)
|
| 188 |
+
flat_df["flat_vector_embed_64_list"] = flat_df.apply(lambda row: convert_to_npfloat64_to_list(row['flat_vector_embed_64']),axis=1)
|
| 189 |
+
|
| 190 |
+
flat_df["genre"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'genre'),axis=1)
|
| 191 |
+
flat_df["album"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'album'),axis=1)
|
| 192 |
+
flat_df["name"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'name'),axis=1)
|
| 193 |
+
flat_df["artist"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'artist_names'),axis=1)
|
| 194 |
+
flat_df["year"] = flat_df.apply(lambda row: look_up_metadata(row['vector_name'], meta_df, 'year'),axis=1)
|
| 195 |
+
flat_df["vector_clip_num"] = flat_df.apply(lambda row: strip_vector_clip(row['vector_name']),axis=1)
|
| 196 |
+
flat_df['embedding_triplet_num'] = flat_df.vector_name.apply(get_triplet_num)
|
| 197 |
+
|
| 198 |
+
flat_dfs.append(flat_df)
|
| 199 |
+
|
| 200 |
+
print("unique songs:", len(flat_df.song_title.unique()))
|
| 201 |
+
|
| 202 |
+
# PINECONE UPLOAD
|
| 203 |
+
|
| 204 |
+
api_key = os.environ['PC_API_KEY']
|
| 205 |
+
pc = Pinecone(api_key=api_key)
|
| 206 |
+
|
| 207 |
+
index_name_clap = f'clap-{index_naming_conv}' # free (comes with plan, can have 100k records)
|
| 208 |
+
index_name_bytecover = f'bytecover-{index_naming_conv}' # free (comes with plan, can have 100k records)
|
| 209 |
+
index_env = 'us-west1-gcp' # NOT free (take down when not in use)
|
| 210 |
+
pod_type = 'p1.x1' # NOT free (take down when not in use)
|
| 211 |
+
|
| 212 |
+
for index_name, flat_df, index_dim in zip([index_name_clap, index_name_bytecover], flat_dfs, [512, 2048]):
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
pc.create_index(
|
| 216 |
+
name=index_name,
|
| 217 |
+
dimension=index_dim,
|
| 218 |
+
metric="cosine",
|
| 219 |
+
spec=PodSpec(
|
| 220 |
+
environment=index_env,
|
| 221 |
+
pod_type=pod_type,
|
| 222 |
+
pods=1
|
| 223 |
+
),
|
| 224 |
+
deletion_protection="disabled"
|
| 225 |
+
)
|
| 226 |
+
except PineconeApiException:
|
| 227 |
+
print(f"WARNING: INDEX {index_name} ALREADY EXISTS")
|
| 228 |
+
time.sleep(5)
|
| 229 |
+
|
| 230 |
+
index = pc.Index(index_name)
|
| 231 |
+
|
| 232 |
+
batch_id = 0
|
| 233 |
+
|
| 234 |
+
df_batcher = BatchGenerator(64)
|
| 235 |
+
|
| 236 |
+
for batch_df in tqdm(df_batcher(flat_df), desc="Uploading batches"):
|
| 237 |
+
#print(batch_df)
|
| 238 |
+
batch_id = batch_id + 1
|
| 239 |
+
index.upsert(vectors=list(zip(batch_df["vector_name"],batch_df["flat_vector_embed_64_list"])))
|
| 240 |
+
|
| 241 |
+
failed_list_update_metadata = []
|
| 242 |
+
|
| 243 |
+
for vec_id in tqdm(range(0,len(flat_df)), desc="Adding metadata"):
|
| 244 |
+
try:
|
| 245 |
+
row = flat_df.iloc[vec_id]
|
| 246 |
+
index.update(id=str(row['vector_name']),
|
| 247 |
+
set_metadata={"genre": row['genre'],
|
| 248 |
+
"song" : row['name'],
|
| 249 |
+
"album": row['album'],
|
| 250 |
+
"artists": row['artist'],
|
| 251 |
+
"year" : str(row['year']),
|
| 252 |
+
"clip_num" : row['vector_clip_num'],
|
| 253 |
+
"triplet_num": str(row['embedding_triplet_num']),
|
| 254 |
+
"spotify_id" : row['song_title']
|
| 255 |
+
})
|
| 256 |
+
except:
|
| 257 |
+
print("failed on:", vec_id)
|
| 258 |
+
failed_list_update_metadata.append(vec_id)
|
| 259 |
+
|
| 260 |
+
pc.create_collection(index_name, index_name)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
if __name__ == "__main__":
|
| 267 |
+
parser = argparse.ArgumentParser(description="Generate ByteCover and CLAP Embeddings for a dataset and put to Pinecone")
|
| 268 |
+
|
| 269 |
+
parser.add_argument('audio_dir')
|
| 270 |
+
parser.add_argument('metadata_dir')
|
| 271 |
+
parser.add_argument('index_name')
|
| 272 |
+
|
| 273 |
+
args = parser.parse_args()
|
| 274 |
+
|
| 275 |
+
generate(args.audio_dir, args.metadata_dir, args.index_name)
|
requirements.txt
CHANGED
|
@@ -5,6 +5,24 @@ loralib
|
|
| 5 |
wavebeat @ git+https://github.com/hugofloresgarcia/wavebeat
|
| 6 |
lac @ git+https://github.com/hugofloresgarcia/lac.git
|
| 7 |
descript-audiotools @ git+https://github.com/hugofloresgarcia/audiotools.git
|
| 8 |
-
-e git+https://github.com/audacitorch/pyharp.git@develop#egg=pyharp
|
| 9 |
torch_pitch_shift
|
| 10 |
-
gradio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
wavebeat @ git+https://github.com/hugofloresgarcia/wavebeat
|
| 6 |
lac @ git+https://github.com/hugofloresgarcia/lac.git
|
| 7 |
descript-audiotools @ git+https://github.com/hugofloresgarcia/audiotools.git
|
|
|
|
| 8 |
torch_pitch_shift
|
| 9 |
+
gradio
|
| 10 |
+
|
| 11 |
+
--extra-index-url https://download.pytorch.org/whl/cu113
|
| 12 |
+
-e git+https://github.com/TEAMuP-dev/pyharp.git@np/overlaycolor#egg=pyharp
|
| 13 |
+
pinecone
|
| 14 |
+
# towhee
|
| 15 |
+
av
|
| 16 |
+
laion_clap
|
| 17 |
+
# pyshorteners
|
| 18 |
+
nnAudio
|
| 19 |
+
ffmpeg-python
|
| 20 |
+
torchvision
|
| 21 |
+
torch
|
| 22 |
+
jsonlines
|
| 23 |
+
wandb
|
| 24 |
+
tqdm
|
| 25 |
+
# For pinecone_generate only
|
| 26 |
+
google-api-python-client
|
| 27 |
+
protoc-gen-openapiv2-protoc3-19
|
| 28 |
+
transformers==4.30.0
|