Spaces:
Runtime error
Runtime error
Commit ·
bb92a2a
0
Parent(s):
initial BACHI deployment
Browse files- Dockerfile +20 -0
- README.md +30 -0
- app.py +236 -0
- dataset.py +382 -0
- models/HT.py +386 -0
- models/__init__.py +0 -0
- models/components.py +339 -0
- models/model.py +686 -0
- models/variants.py +654 -0
- packages.txt +1 -0
- requirements.txt +10 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY packages.txt /app/packages.txt
|
| 6 |
+
RUN apt-get update && \
|
| 7 |
+
apt-get install -y --no-install-recommends git gcc libc6-dev && \
|
| 8 |
+
xargs apt-get install -y --no-install-recommends < /app/packages.txt && \
|
| 9 |
+
rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
COPY requirements.txt /app/requirements.txt
|
| 12 |
+
RUN pip install --no-cache-dir gradio==5.28.0
|
| 13 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 14 |
+
|
| 15 |
+
COPY . /app
|
| 16 |
+
|
| 17 |
+
ENV PORT=7860
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Bachi
|
| 3 |
+
emoji: 🎵
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# BACHI — HARP Endpoint
|
| 12 |
+
|
| 13 |
+
Deploys [BACHI](https://github.com/AndyWeasley2004/BACHI_Chord_Recognition) as a [PyHARP](https://github.com/TEAMuP-dev/pyharp) endpoint.
|
| 14 |
+
|
| 15 |
+
## What it does
|
| 16 |
+
|
| 17 |
+
Automatic chord recognition from symbolic music scores. Outputs beat-aligned chord labels (root, quality, bass).
|
| 18 |
+
|
| 19 |
+
## Inputs
|
| 20 |
+
|
| 21 |
+
- MIDI or MusicXML file (.mid, .midi, .musicxml, .mxl, .xml)
|
| 22 |
+
- Model type: Classical or Pop
|
| 23 |
+
|
| 24 |
+
## Note
|
| 25 |
+
|
| 26 |
+
Model is trained on piano data only and supports MIDI pitch range 21-108.
|
| 27 |
+
|
| 28 |
+
## License
|
| 29 |
+
|
| 30 |
+
MIT
|
app.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import math
|
| 4 |
+
import tempfile
|
| 5 |
+
import pickle
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import numpy as np
|
| 10 |
+
import yaml
|
| 11 |
+
import gradio as gr
|
| 12 |
+
from pyharp import ModelCard, build_endpoint
|
| 13 |
+
from huggingface_hub import hf_hub_download
|
| 14 |
+
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 16 |
+
from dataset import load_vocabs
|
| 17 |
+
from models.variants import build_model
|
| 18 |
+
|
| 19 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "true"
|
| 20 |
+
|
| 21 |
+
REPO_ID = "Itsuki-music/BACHI_Chord_Recognition"
|
| 22 |
+
CHECKPOINT_NAMES = {
|
| 23 |
+
"Classical": "classical_film_kdec",
|
| 24 |
+
"Pop": "pop909_film_kdec",
|
| 25 |
+
}
|
| 26 |
+
CHECKPOINT_FILES = ["best_model.pt", "config.yaml", "vocab.pkl"]
|
| 27 |
+
|
| 28 |
+
loaded_models = {}
|
| 29 |
+
|
| 30 |
+
def get_model(model_type: str):
|
| 31 |
+
if model_type not in loaded_models:
|
| 32 |
+
folder = CHECKPOINT_NAMES[model_type]
|
| 33 |
+
ckpt_dir = Path(__file__).parent / "ckpts" / folder
|
| 34 |
+
ckpt_dir.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
|
| 36 |
+
for fname in CHECKPOINT_FILES:
|
| 37 |
+
dest = ckpt_dir / fname
|
| 38 |
+
if not dest.exists():
|
| 39 |
+
print(f"Downloading {folder}/{fname}...", flush=True)
|
| 40 |
+
hf_hub_download(
|
| 41 |
+
repo_id=REPO_ID,
|
| 42 |
+
filename=f"{folder}/{fname}",
|
| 43 |
+
repo_type="dataset",
|
| 44 |
+
local_dir=Path(__file__).parent / "ckpts",
|
| 45 |
+
)
|
| 46 |
+
print(f"Downloaded {fname}.", flush=True)
|
| 47 |
+
|
| 48 |
+
with open(ckpt_dir / "config.yaml", "r") as f:
|
| 49 |
+
config = yaml.safe_load(f)
|
| 50 |
+
|
| 51 |
+
vocabs = load_vocabs(str(ckpt_dir / "vocab.pkl"))
|
| 52 |
+
use_key = (
|
| 53 |
+
bool(config.get("use_key", False))
|
| 54 |
+
or bool(config["training"].get("use_key", False))
|
| 55 |
+
or bool(config["model"].get("use_key", False))
|
| 56 |
+
)
|
| 57 |
+
experiment = config["experiment"]
|
| 58 |
+
|
| 59 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 60 |
+
print(f"Loading {model_type} model...", flush=True)
|
| 61 |
+
model = build_model(experiment, config["model"], vocabs, use_key=use_key).to(device)
|
| 62 |
+
model.load_state_dict(
|
| 63 |
+
torch.load(ckpt_dir / "best_model.pt", map_location=device, weights_only=True)
|
| 64 |
+
)
|
| 65 |
+
model.eval()
|
| 66 |
+
print(f"{model_type} model loaded.", flush=True)
|
| 67 |
+
|
| 68 |
+
loaded_models[model_type] = (model, config, vocabs, use_key, device)
|
| 69 |
+
|
| 70 |
+
return loaded_models[model_type]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def extract_pianoroll(score_path: Path, resolution: int = 12):
|
| 74 |
+
import miditoolkit
|
| 75 |
+
from music21 import converter, note as m21_note, chord as m21_chord
|
| 76 |
+
|
| 77 |
+
notes_data = []
|
| 78 |
+
suffix = score_path.suffix.lower()
|
| 79 |
+
|
| 80 |
+
if suffix in {".mid", ".midi"}:
|
| 81 |
+
midi = miditoolkit.MidiFile(str(score_path))
|
| 82 |
+
tpb = midi.ticks_per_beat or 480
|
| 83 |
+
for inst in midi.instruments:
|
| 84 |
+
if inst.is_drum:
|
| 85 |
+
continue
|
| 86 |
+
for n in inst.notes:
|
| 87 |
+
notes_data.append((n.pitch, n.start / tpb, n.end / tpb))
|
| 88 |
+
else:
|
| 89 |
+
sc = converter.parse(str(score_path))
|
| 90 |
+
parts = list(sc.parts) if sc.hasPartLikeStreams() else [sc]
|
| 91 |
+
for part in parts:
|
| 92 |
+
inst = part.getInstrument()
|
| 93 |
+
if inst:
|
| 94 |
+
classes = inst.classes if hasattr(inst, "classes") else []
|
| 95 |
+
if "Percussion" in classes or "Unpitched" in classes:
|
| 96 |
+
continue
|
| 97 |
+
for el in part.flat.notes:
|
| 98 |
+
dur = float(el.quarterLength)
|
| 99 |
+
start = float(el.offset)
|
| 100 |
+
if isinstance(el, m21_note.Note):
|
| 101 |
+
notes_data.append((el.pitch.midi, start, start + dur))
|
| 102 |
+
elif isinstance(el, m21_chord.Chord):
|
| 103 |
+
for p in el.pitches:
|
| 104 |
+
notes_data.append((p.midi, start, start + dur))
|
| 105 |
+
|
| 106 |
+
if not notes_data:
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
notes_data.sort(key=lambda x: x[1])
|
| 110 |
+
last_end = max(nd[2] for nd in notes_data)
|
| 111 |
+
total_frames = math.ceil(last_end * resolution)
|
| 112 |
+
pianoroll = np.zeros((88, total_frames), dtype=np.int8)
|
| 113 |
+
for midi_pitch, start_b, end_b in notes_data:
|
| 114 |
+
row = midi_pitch - 21
|
| 115 |
+
if not (0 <= row < 88):
|
| 116 |
+
continue
|
| 117 |
+
s_f = max(0, math.floor(start_b * resolution))
|
| 118 |
+
e_f = min(total_frames, math.ceil(end_b * resolution))
|
| 119 |
+
pianoroll[row, s_f:e_f] = 1
|
| 120 |
+
|
| 121 |
+
return pianoroll
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def predict_piece(pianoroll, model, config, vocabs, use_key, device):
|
| 125 |
+
beat_resolution = config["model"]["beat_resolution"]
|
| 126 |
+
label_resolution = config["model"]["label_resolution"]
|
| 127 |
+
segment_len = config["model"]["n_beats"] * beat_resolution
|
| 128 |
+
pr_to_label_ratio = beat_resolution // label_resolution
|
| 129 |
+
comps_eval = ["root", "quality", "bass"] + (["key"] if use_key else [])
|
| 130 |
+
|
| 131 |
+
n_frames = pianoroll.shape[0]
|
| 132 |
+
segments, masks = [], []
|
| 133 |
+
for i in range(0, n_frames, segment_len):
|
| 134 |
+
seg = pianoroll[i : i + segment_len]
|
| 135 |
+
orig_len = seg.shape[0]
|
| 136 |
+
if orig_len < segment_len:
|
| 137 |
+
seg = torch.cat([seg, torch.zeros(segment_len - orig_len, seg.shape[1])], dim=0)
|
| 138 |
+
mask = torch.ones(segment_len, dtype=torch.bool)
|
| 139 |
+
if orig_len < segment_len:
|
| 140 |
+
mask[orig_len:] = False
|
| 141 |
+
segments.append(seg)
|
| 142 |
+
masks.append(mask)
|
| 143 |
+
|
| 144 |
+
piece_preds = {k: [] for k in comps_eval + ["boundary"]}
|
| 145 |
+
for i in range(0, len(segments), 16):
|
| 146 |
+
batch_segs = torch.stack(segments[i : i + 16]).to(device)
|
| 147 |
+
batch_masks = torch.stack(masks[i : i + 16]).to(device)
|
| 148 |
+
with torch.no_grad():
|
| 149 |
+
out = model.forward_infer(batch_segs, src_key_padding_mask=~batch_masks)
|
| 150 |
+
for k in comps_eval + ["boundary"]:
|
| 151 |
+
if k in out:
|
| 152 |
+
piece_preds[k].append(out[k].detach().cpu())
|
| 153 |
+
|
| 154 |
+
n_target = math.ceil(n_frames / pr_to_label_ratio)
|
| 155 |
+
piece_pred_ids = {}
|
| 156 |
+
for k, parts in piece_preds.items():
|
| 157 |
+
if not parts:
|
| 158 |
+
continue
|
| 159 |
+
cat = torch.cat([p.reshape(-1) for p in parts], dim=0)
|
| 160 |
+
piece_pred_ids[k] = cat[:n_target] if k == "boundary" else cat[:n_target].long()
|
| 161 |
+
|
| 162 |
+
inv_root = {v: k for k, v in vocabs["root"].items()}
|
| 163 |
+
inv_qual = {v: k for k, v in vocabs["quality"].items()}
|
| 164 |
+
inv_bass = {v: k for k, v in vocabs["bass"].items()}
|
| 165 |
+
|
| 166 |
+
valid_len = len(piece_pred_ids.get("root", []))
|
| 167 |
+
if valid_len == 0:
|
| 168 |
+
return "No predictions generated."
|
| 169 |
+
|
| 170 |
+
r_seq = piece_pred_ids["root"][:valid_len].tolist()
|
| 171 |
+
q_seq = piece_pred_ids["quality"][:valid_len].tolist()
|
| 172 |
+
b_seq = piece_pred_ids["bass"][:valid_len].tolist()
|
| 173 |
+
|
| 174 |
+
time_per_token = 1.0 / max(1, config["model"]["label_resolution"])
|
| 175 |
+
merged = []
|
| 176 |
+
cur_r, cur_q, cur_b = r_seq[0], q_seq[0], b_seq[0]
|
| 177 |
+
cur_start = 0
|
| 178 |
+
for t in range(1, valid_len):
|
| 179 |
+
if r_seq[t] != cur_r or q_seq[t] != cur_q or b_seq[t] != cur_b:
|
| 180 |
+
label = f"{inv_root.get(cur_r, str(cur_r))}_{inv_qual.get(cur_q, str(cur_q))}_{inv_bass.get(cur_b, str(cur_b))}"
|
| 181 |
+
merged.append(f"{cur_start * time_per_token:.2f} {label}")
|
| 182 |
+
cur_r, cur_q, cur_b = r_seq[t], q_seq[t], b_seq[t]
|
| 183 |
+
cur_start = t
|
| 184 |
+
label = f"{inv_root.get(cur_r, str(cur_r))}_{inv_qual.get(cur_q, str(cur_q))}_{inv_bass.get(cur_b, str(cur_b))}"
|
| 185 |
+
merged.append(f"{cur_start * time_per_token:.2f} {label}")
|
| 186 |
+
|
| 187 |
+
return "\n".join(merged)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
model_card = ModelCard(
|
| 191 |
+
name="BACHI Chord Recognition",
|
| 192 |
+
description="Automatic chord recognition from symbolic music scores (MIDI or MusicXML). Outputs beat-aligned chord labels.",
|
| 193 |
+
author="Mingyang Yao, Ke Chen, Shlomo Dubnov, Taylor Berg-Kirkpatrick",
|
| 194 |
+
tags=["chord-recognition", "symbolic-music", "midi", "musicxml"],
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def process_fn(input_file: str, model_type: str) -> str:
|
| 199 |
+
print(f"Processing with {model_type} model...", flush=True)
|
| 200 |
+
model, config, vocabs, use_key, device = get_model(model_type)
|
| 201 |
+
|
| 202 |
+
score_path = Path(input_file)
|
| 203 |
+
pianoroll_np = extract_pianoroll(score_path, resolution=config["model"]["beat_resolution"])
|
| 204 |
+
if pianoroll_np is None:
|
| 205 |
+
return "Error: Could not extract notes from the input file."
|
| 206 |
+
|
| 207 |
+
pianoroll = torch.from_numpy(pianoroll_np.T).float()
|
| 208 |
+
result = predict_piece(pianoroll, model, config, vocabs, use_key, device)
|
| 209 |
+
print("Done.", flush=True)
|
| 210 |
+
return result
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
with gr.Blocks() as demo:
|
| 214 |
+
input_components = [
|
| 215 |
+
gr.File(
|
| 216 |
+
label="Input Score (.mid, .midi, .musicxml, .mxl, .xml)",
|
| 217 |
+
file_types=[".mid", ".midi", ".musicxml", ".mxl", ".xml"],
|
| 218 |
+
),
|
| 219 |
+
gr.Dropdown(
|
| 220 |
+
choices=["Classical", "Pop"],
|
| 221 |
+
value="Classical",
|
| 222 |
+
label="Model Type",
|
| 223 |
+
),
|
| 224 |
+
]
|
| 225 |
+
output_components = [
|
| 226 |
+
gr.Textbox(label="Chord Predictions", lines=20),
|
| 227 |
+
]
|
| 228 |
+
app = build_endpoint(
|
| 229 |
+
model_card=model_card,
|
| 230 |
+
input_components=input_components,
|
| 231 |
+
output_components=output_components,
|
| 232 |
+
process_fn=process_fn,
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
print("Launching Gradio...", flush=True)
|
| 236 |
+
demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_error=True, pwa=True)
|
dataset.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import torch
|
| 4 |
+
from torch.utils.data import Dataset, random_split
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pickle
|
| 7 |
+
from typing import List, Dict, Tuple, Optional, Any
|
| 8 |
+
import math
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
import random
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def load_vocabs(vocab_path: str) -> Dict[str, Any]:
|
| 15 |
+
"""Loads vocabularies and augments with per-component PAD/NONE indices.
|
| 16 |
+
|
| 17 |
+
For 3-part prediction, only `root`, `quality`, and `bass` are loaded.
|
| 18 |
+
"""
|
| 19 |
+
with open(vocab_path, 'rb') as f:
|
| 20 |
+
data = pickle.load(f)
|
| 21 |
+
|
| 22 |
+
root_map = data['root_to_idx']
|
| 23 |
+
pad_token = 'PAD'
|
| 24 |
+
none_tokens = ['N', 'None'] # allow either spelling in source vocabs
|
| 25 |
+
bass_map = root_map
|
| 26 |
+
|
| 27 |
+
# Only keep the three chord parts for prediction
|
| 28 |
+
vocabs = {
|
| 29 |
+
'root': root_map,
|
| 30 |
+
'quality': data['quality_to_idx'],
|
| 31 |
+
'bass': bass_map,
|
| 32 |
+
'key': data['key_to_idx'],
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
# Global root PAD index (back-compat)
|
| 36 |
+
vocabs['pad_idx'] = root_map[pad_token]
|
| 37 |
+
|
| 38 |
+
# Add per-component PAD and NONE indices
|
| 39 |
+
for comp, comp_map in list(vocabs.items()):
|
| 40 |
+
if comp == 'pad_idx':
|
| 41 |
+
continue
|
| 42 |
+
# per-component PAD index (must exist)
|
| 43 |
+
comp_pad_idx = comp_map.get(pad_token)
|
| 44 |
+
if comp_pad_idx is None:
|
| 45 |
+
raise ValueError(f"Component '{comp}' vocab lacks PAD token")
|
| 46 |
+
vocabs[f'{comp}_pad_idx'] = comp_pad_idx
|
| 47 |
+
|
| 48 |
+
# NONE index preference: N > None > PAD
|
| 49 |
+
none_idx = None
|
| 50 |
+
for tok in none_tokens:
|
| 51 |
+
if tok in comp_map:
|
| 52 |
+
none_idx = comp_map[tok]
|
| 53 |
+
break
|
| 54 |
+
if none_idx is None:
|
| 55 |
+
none_idx = comp_pad_idx
|
| 56 |
+
vocabs[f'{comp}_none_idx'] = none_idx
|
| 57 |
+
|
| 58 |
+
return vocabs
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class PianoRollDataset(Dataset):
|
| 62 |
+
"""Dataset for piano roll representation."""
|
| 63 |
+
pad_idx = -1 # Will be updated in __init__
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
data_root: str,
|
| 67 |
+
config: dict,
|
| 68 |
+
vocabs: Dict[str, Any],
|
| 69 |
+
split: str = 'train',
|
| 70 |
+
use_augmentation: bool = False,
|
| 71 |
+
use_key: bool = False,
|
| 72 |
+
):
|
| 73 |
+
self.data_root = data_root
|
| 74 |
+
self.config = config
|
| 75 |
+
self.n_beats = self.config['n_beats']
|
| 76 |
+
self.split = split
|
| 77 |
+
self.use_augmentation = use_augmentation
|
| 78 |
+
self.use_key = use_key
|
| 79 |
+
self.beat_resolution = self.config['beat_resolution']
|
| 80 |
+
self.label_resolution = self.config['label_resolution']
|
| 81 |
+
self.pr_to_label_ratio = self.beat_resolution // self.label_resolution
|
| 82 |
+
|
| 83 |
+
self.vocabs = vocabs
|
| 84 |
+
self.pad_idx = self.vocabs['pad_idx']
|
| 85 |
+
|
| 86 |
+
self.chord_components = ['root', 'quality', 'bass']
|
| 87 |
+
self.label_indices_map = {'root': 0, 'quality': 1, 'bass': 2}
|
| 88 |
+
if self.use_key:
|
| 89 |
+
self.chord_components.append('key')
|
| 90 |
+
self.label_indices_map['key'] = 3
|
| 91 |
+
|
| 92 |
+
# --- Lengths in pianoroll-frame resolution ---
|
| 93 |
+
self.max_len = self.n_beats * self.beat_resolution
|
| 94 |
+
|
| 95 |
+
for comp in self.chord_components:
|
| 96 |
+
setattr(self, f'{comp}_vocab', self.vocabs[comp])
|
| 97 |
+
setattr(self, f'{comp}_none_idx', self.vocabs[f'{comp}_none_idx'])
|
| 98 |
+
|
| 99 |
+
suffix = 'shift0.npz' if not self.use_augmentation else '.npz'
|
| 100 |
+
# print(f"Loading {suffix} files from {data_root}")
|
| 101 |
+
self.file_list = sorted([
|
| 102 |
+
os.path.join(data_root, f)
|
| 103 |
+
for f in os.listdir(data_root) if f.endswith(suffix)
|
| 104 |
+
])
|
| 105 |
+
|
| 106 |
+
def __len__(self) -> int:
|
| 107 |
+
return len(self.file_list)
|
| 108 |
+
|
| 109 |
+
def __getitem__(self, idx: int) -> Optional[Dict[str, torch.Tensor]]:
|
| 110 |
+
filepath = self.file_list[idx]
|
| 111 |
+
with np.load(filepath, allow_pickle=True) as data:
|
| 112 |
+
pianoroll_full = torch.from_numpy(data['pianoroll'].T).float()
|
| 113 |
+
labels_full = data['labels']
|
| 114 |
+
boundaries_full = data['boundaries']
|
| 115 |
+
|
| 116 |
+
pianoroll = pianoroll_full
|
| 117 |
+
labels = labels_full
|
| 118 |
+
|
| 119 |
+
# --- Create ground truth chord tensor from labels (map to per-component vocab indices) ---
|
| 120 |
+
target_indices = {}
|
| 121 |
+
|
| 122 |
+
for comp in self.chord_components:
|
| 123 |
+
vocab = getattr(self, f'{comp}_vocab')
|
| 124 |
+
none_idx = getattr(self, f'{comp}_none_idx')
|
| 125 |
+
label_col_idx = self.label_indices_map[comp]
|
| 126 |
+
col = labels[:, label_col_idx]
|
| 127 |
+
mapped_tensor = None
|
| 128 |
+
# If labels are already integer indices within range, accept directly
|
| 129 |
+
try:
|
| 130 |
+
if np.issubdtype(col.dtype, np.integer):
|
| 131 |
+
col_int = col.astype(np.int64)
|
| 132 |
+
if col_int.min(initial=0) >= 0 and col_int.max(initial=0) < len(vocab):
|
| 133 |
+
mapped_tensor = torch.from_numpy(col_int)
|
| 134 |
+
except Exception:
|
| 135 |
+
mapped_tensor = None
|
| 136 |
+
# Otherwise map string/mixed labels through vocab with fallback to none_idx
|
| 137 |
+
if mapped_tensor is None:
|
| 138 |
+
try:
|
| 139 |
+
col_list = col.astype(str).tolist()
|
| 140 |
+
except Exception:
|
| 141 |
+
col_list = [str(x) for x in col.tolist()]
|
| 142 |
+
mapped = [vocab.get(lbl, none_idx) for lbl in col_list]
|
| 143 |
+
mapped_tensor = torch.tensor(mapped, dtype=torch.long)
|
| 144 |
+
target_indices[comp] = mapped_tensor.long()
|
| 145 |
+
|
| 146 |
+
# --- Load pre-computed boundary flag ---
|
| 147 |
+
boundary_flag = torch.from_numpy(boundaries_full.astype(np.float32))
|
| 148 |
+
|
| 149 |
+
if self.split == 'train':
|
| 150 |
+
return self._get_train_item(pianoroll, target_indices, boundary_flag)
|
| 151 |
+
else: # 'val' or 'test'
|
| 152 |
+
piece_name = _get_piece_name(filepath)
|
| 153 |
+
# Build accurate targets from labels for evaluation
|
| 154 |
+
return self._get_eval_item(pianoroll, labels, boundary_flag, piece_name)
|
| 155 |
+
|
| 156 |
+
def _sample_stratified_start(self, X: int) -> int:
|
| 157 |
+
"""
|
| 158 |
+
Sample s ∈ {0..X} with P(s) ∝ 1 + beta * (s/X).
|
| 159 |
+
Implemented as a mixture of Uniform and 'linear-in-s' discrete law.
|
| 160 |
+
Exact, O(1), numerically stable.
|
| 161 |
+
|
| 162 |
+
beta ∈ [0,2]. beta=0 -> uniform; beta=1 -> mild late tilt (good default).
|
| 163 |
+
"""
|
| 164 |
+
if X <= 0:
|
| 165 |
+
return 0
|
| 166 |
+
|
| 167 |
+
beta = float(getattr(self, 'stratify_beta', 1.0))
|
| 168 |
+
|
| 169 |
+
# Mixture weights: P = a * Uniform + (1-a) * Linear(s)
|
| 170 |
+
a = 1.0 - beta / 2.0 # ∈ [0,1]
|
| 171 |
+
if np.random.rand() < a:
|
| 172 |
+
# Uniform over 0..X
|
| 173 |
+
return int(np.random.randint(0, X + 1))
|
| 174 |
+
else:
|
| 175 |
+
# Sample from Q(s) ∝ s over {0..X} (i.e., s=0 has weight 0).
|
| 176 |
+
# Do it by inverting triangular numbers over 1..X.
|
| 177 |
+
M = X * (X + 1) // 2 # sum_{s=1}^X s
|
| 178 |
+
r = np.random.randint(1, M + 1) # 1..M inclusive
|
| 179 |
+
s = int((math.isqrt(1 + 8 * r) - 1) // 2) # floor((sqrt(1+8r)-1)/2)
|
| 180 |
+
# Numerical guard (rare when r hits exact triangle): clamp
|
| 181 |
+
if s > X:
|
| 182 |
+
s = X
|
| 183 |
+
return s
|
| 184 |
+
|
| 185 |
+
def _get_train_item(self, pianoroll, target_indices, boundary_flag):
|
| 186 |
+
n_pr_frames = pianoroll.shape[0]
|
| 187 |
+
# start with at least half of window size and convert to label frames
|
| 188 |
+
max_start_label_frame = (n_pr_frames - self.max_len // 2) // self.pr_to_label_ratio
|
| 189 |
+
target_max_len = self.max_len // self.pr_to_label_ratio
|
| 190 |
+
|
| 191 |
+
# Stratified start over 0..max_start_label_frame (tilt to late positions)
|
| 192 |
+
start_label_frame = self._sample_stratified_start(max_start_label_frame)
|
| 193 |
+
start_pr_frame = start_label_frame * self.pr_to_label_ratio
|
| 194 |
+
|
| 195 |
+
# --- slice & pad encoder input ---
|
| 196 |
+
pr_segment = pianoroll[start_pr_frame : start_pr_frame + self.max_len]
|
| 197 |
+
pr_pad_amount = self.max_len - pr_segment.shape[0]
|
| 198 |
+
if pr_pad_amount > 0:
|
| 199 |
+
# keep dtype/device consistent with pr_segment
|
| 200 |
+
pr_pad = pr_segment.new_zeros((pr_pad_amount, pr_segment.shape[1]))
|
| 201 |
+
pr_segment = torch.cat([pr_segment, pr_pad], dim=0)
|
| 202 |
+
|
| 203 |
+
# --- slice targets at label resolution ---
|
| 204 |
+
target_start = start_label_frame
|
| 205 |
+
target_segs = {}
|
| 206 |
+
for comp in self.chord_components:
|
| 207 |
+
target_segs[comp] = target_indices[comp][target_start : target_start + target_max_len]
|
| 208 |
+
boundary_seg = boundary_flag[target_start : target_start + target_max_len]
|
| 209 |
+
|
| 210 |
+
# --- masks & padding for targets ---
|
| 211 |
+
current_target_len = target_segs[self.chord_components[0]].shape[0]
|
| 212 |
+
target_mask = torch.zeros(target_max_len, dtype=torch.bool)
|
| 213 |
+
target_mask[:current_target_len] = True
|
| 214 |
+
|
| 215 |
+
# expand target mask to encoder (frame) mask
|
| 216 |
+
encoder_mask = target_mask.repeat_interleave(self.pr_to_label_ratio)
|
| 217 |
+
if pr_pad_amount > 0:
|
| 218 |
+
encoder_mask[-pr_pad_amount:] = False
|
| 219 |
+
|
| 220 |
+
target_pad_amount = target_max_len - current_target_len
|
| 221 |
+
if target_pad_amount > 0:
|
| 222 |
+
for comp in self.chord_components:
|
| 223 |
+
comp_none_idx = getattr(self, f'{comp}_none_idx')
|
| 224 |
+
pad_tensor = torch.full((target_pad_amount,), comp_none_idx, dtype=torch.long)
|
| 225 |
+
target_segs[comp] = torch.cat([target_segs[comp], pad_tensor])
|
| 226 |
+
|
| 227 |
+
boundary_pad = torch.zeros(target_pad_amount, dtype=boundary_seg.dtype)
|
| 228 |
+
boundary_seg = torch.cat([boundary_seg, boundary_pad])
|
| 229 |
+
|
| 230 |
+
item = {
|
| 231 |
+
'encoder_input': pr_segment,
|
| 232 |
+
'target_boundary': boundary_seg,
|
| 233 |
+
'mask': target_mask,
|
| 234 |
+
'encoder_mask': encoder_mask,
|
| 235 |
+
}
|
| 236 |
+
for comp in self.chord_components:
|
| 237 |
+
item[f'target_{comp}'] = target_segs[comp]
|
| 238 |
+
|
| 239 |
+
return item
|
| 240 |
+
|
| 241 |
+
def _get_eval_item(self, pianoroll, labels, boundary_flag, piece_name):
|
| 242 |
+
# Reconstruct per-component target indices directly from the label matrix
|
| 243 |
+
n_label_frames = labels.shape[0]
|
| 244 |
+
target_indices = {}
|
| 245 |
+
for comp in self.chord_components:
|
| 246 |
+
vocab = getattr(self, f'{comp}_vocab')
|
| 247 |
+
none_idx = getattr(self, f'{comp}_none_idx')
|
| 248 |
+
label_col_idx = self.label_indices_map[comp]
|
| 249 |
+
# Extract the column for this component; handle types robustly
|
| 250 |
+
col = labels[:, label_col_idx]
|
| 251 |
+
mapped_tensor = None
|
| 252 |
+
# Case 1: already integer indices
|
| 253 |
+
try:
|
| 254 |
+
if np.issubdtype(col.dtype, np.integer):
|
| 255 |
+
col_int = col.astype(np.int64)
|
| 256 |
+
# If values look like valid indices, accept directly; otherwise fallback to mapping
|
| 257 |
+
if col_int.min(initial=0) >= 0 and col_int.max(initial=0) < len(vocab):
|
| 258 |
+
mapped_tensor = torch.from_numpy(col_int)
|
| 259 |
+
except Exception:
|
| 260 |
+
mapped_tensor = None
|
| 261 |
+
# Case 2: map from labels (strings or mixed types) to indices
|
| 262 |
+
if mapped_tensor is None:
|
| 263 |
+
try:
|
| 264 |
+
col_list = col.astype(str).tolist()
|
| 265 |
+
except Exception:
|
| 266 |
+
col_list = [str(x) for x in col.tolist()]
|
| 267 |
+
mapped = [vocab.get(lbl, none_idx) for lbl in col_list]
|
| 268 |
+
mapped_tensor = torch.tensor(mapped, dtype=torch.long)
|
| 269 |
+
target_indices[comp] = mapped_tensor.long()
|
| 270 |
+
|
| 271 |
+
mask = torch.ones(n_label_frames, dtype=torch.bool)
|
| 272 |
+
encoder_mask = torch.ones(pianoroll.shape[0], dtype=torch.bool)
|
| 273 |
+
item = {
|
| 274 |
+
'piece_name': piece_name,
|
| 275 |
+
'encoder_input': pianoroll,
|
| 276 |
+
'target_boundary': boundary_flag,
|
| 277 |
+
'mask': mask,
|
| 278 |
+
'encoder_mask': encoder_mask,
|
| 279 |
+
}
|
| 280 |
+
for comp in self.chord_components:
|
| 281 |
+
item[f'target_{comp}'] = target_indices[comp]
|
| 282 |
+
return item
|
| 283 |
+
|
| 284 |
+
def get_vocab_sizes(self) -> Dict[str, int]:
|
| 285 |
+
sizes = {comp: len(self.vocabs[comp]) for comp in self.chord_components}
|
| 286 |
+
return sizes
|
| 287 |
+
|
| 288 |
+
def get_pad_idx(self) -> int:
|
| 289 |
+
return self.pad_idx
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def _get_piece_name(filename: str) -> str:
|
| 293 |
+
"""Extracts the base piece name from a filename by splitting on '_shift'."""
|
| 294 |
+
base_filename = os.path.basename(filename)
|
| 295 |
+
if '_shift' in base_filename:
|
| 296 |
+
piece_name = base_filename.split('_shift')[0]
|
| 297 |
+
else:
|
| 298 |
+
piece_name = base_filename
|
| 299 |
+
return piece_name
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def create_datasets(
|
| 303 |
+
data_root: str,
|
| 304 |
+
config: dict,
|
| 305 |
+
vocabs: Dict[str, Any],
|
| 306 |
+
seed: int = 42,
|
| 307 |
+
) -> Tuple[Dataset, Dataset]:
|
| 308 |
+
"""
|
| 309 |
+
Create train and validation datasets with group-based splitting.
|
| 310 |
+
This ensures that all augmentations of a piece belong to the same split.
|
| 311 |
+
"""
|
| 312 |
+
full_dataset = PianoRollDataset(
|
| 313 |
+
data_root=data_root,
|
| 314 |
+
config=config,
|
| 315 |
+
vocabs=vocabs,
|
| 316 |
+
split='train', # split does not matter here
|
| 317 |
+
use_augmentation=config['use_augmentation'],
|
| 318 |
+
use_key=config['use_key'],
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
# Group files by piece name
|
| 322 |
+
piece_files = defaultdict(list)
|
| 323 |
+
for f in full_dataset.file_list:
|
| 324 |
+
piece_name = _get_piece_name(f)
|
| 325 |
+
piece_files[piece_name].append(f)
|
| 326 |
+
|
| 327 |
+
unique_pieces = sorted(list(piece_files.keys()))
|
| 328 |
+
|
| 329 |
+
# Shuffle for random split
|
| 330 |
+
random.seed(seed)
|
| 331 |
+
random.shuffle(unique_pieces)
|
| 332 |
+
|
| 333 |
+
# Split unique pieces (90% train, 10% validation)
|
| 334 |
+
train_size = int(0.9 * len(unique_pieces))
|
| 335 |
+
train_pieces = unique_pieces[:train_size]
|
| 336 |
+
val_pieces = unique_pieces[train_size:]
|
| 337 |
+
|
| 338 |
+
# Get file lists for each split, only use shift0.npz for validation
|
| 339 |
+
train_files = [file for piece in train_pieces for file in piece_files[piece]]
|
| 340 |
+
if config['use_augmentation']:
|
| 341 |
+
val_files = [file for piece in val_pieces for file in piece_files[piece] if file.endswith('shift0.npz')]
|
| 342 |
+
else:
|
| 343 |
+
val_files = [file for piece in val_pieces for file in piece_files[piece]]
|
| 344 |
+
print(f"Train files: {len(train_files)}, Val files: {len(val_files)}")
|
| 345 |
+
|
| 346 |
+
# Create datasets for each split with the correct file list
|
| 347 |
+
train_dataset = PianoRollDataset(data_root, config, vocabs, 'train', use_key=config['use_key'])
|
| 348 |
+
train_dataset.file_list = train_files
|
| 349 |
+
|
| 350 |
+
val_dataset = PianoRollDataset(data_root, config, vocabs, 'val', use_key=config['use_key'])
|
| 351 |
+
val_dataset.file_list = val_files
|
| 352 |
+
|
| 353 |
+
json.dump(sorted([_get_piece_name(file) for file in val_files]),
|
| 354 |
+
open('val_files_unique.json', 'w'), indent=2)
|
| 355 |
+
|
| 356 |
+
return train_dataset, val_dataset
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def collate_fn(batch):
|
| 360 |
+
"""
|
| 361 |
+
Collate function that filters out empty or invalid samples.
|
| 362 |
+
For training, it uses default collate.
|
| 363 |
+
For evaluation (variable length), it handles padding if needed, but typically used with batch_size=1.
|
| 364 |
+
"""
|
| 365 |
+
batch = [item for item in batch if item is not None]
|
| 366 |
+
if not batch:
|
| 367 |
+
return {}
|
| 368 |
+
|
| 369 |
+
# If batch contains only a single sample, simply return that sample's dict.
|
| 370 |
+
# This is handy for evaluation where we usually set batch_size = 1 and do
|
| 371 |
+
# not need the extra list wrapper.
|
| 372 |
+
if len(batch) == 1 and 'piece_name' in batch[0]:
|
| 373 |
+
return batch[0]
|
| 374 |
+
|
| 375 |
+
# For training batches (fixed-length segments) every sample has the same
|
| 376 |
+
# sequence length, so the default PyTorch collate works fine.
|
| 377 |
+
if 'encoder_input' in batch[0] and batch[0]['encoder_input'].shape[0] == batch[-1]['encoder_input'].shape[0]:
|
| 378 |
+
return torch.utils.data.dataloader.default_collate(batch)
|
| 379 |
+
|
| 380 |
+
# Otherwise we have variable-length sequences – fall back to returning the
|
| 381 |
+
# list so the caller can deal with padding/iteration manually.
|
| 382 |
+
return batch
|
models/HT.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import math
|
| 5 |
+
from typing import Dict, Optional, Tuple
|
| 6 |
+
|
| 7 |
+
def get_relative_position_encoding(n_steps, d_model, max_dist=10):
|
| 8 |
+
"""
|
| 9 |
+
Generates relative positional encodings, similar to Transformer-XL.
|
| 10 |
+
"""
|
| 11 |
+
vocab_size = 2 * max_dist + 1
|
| 12 |
+
position = torch.arange(0, vocab_size, dtype=torch.float).unsqueeze(1)
|
| 13 |
+
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
| 14 |
+
|
| 15 |
+
pe = torch.zeros(vocab_size, d_model)
|
| 16 |
+
pe[:, 0::2] = torch.sin(position * div_term)
|
| 17 |
+
pe[:, 1::2] = torch.cos(position * div_term)
|
| 18 |
+
|
| 19 |
+
range_vec = torch.arange(n_steps)
|
| 20 |
+
distance_mat = range_vec[None, :] - range_vec[:, None]
|
| 21 |
+
distance_mat_clipped = torch.clamp(distance_mat, -max_dist, max_dist)
|
| 22 |
+
final_mat = distance_mat_clipped + max_dist
|
| 23 |
+
|
| 24 |
+
embeddings = F.embedding(final_mat.long(), pe)
|
| 25 |
+
return embeddings
|
| 26 |
+
|
| 27 |
+
class RelativeMultiHeadAttention(nn.Module):
|
| 28 |
+
"""
|
| 29 |
+
Multi-Head Attention with relative positional encoding, inspired by Transformer-XL and HTv2.
|
| 30 |
+
"""
|
| 31 |
+
def __init__(self, d_model, n_heads, dropout=0.1, max_dist=10):
|
| 32 |
+
super().__init__()
|
| 33 |
+
assert d_model % n_heads == 0
|
| 34 |
+
self.d_model = d_model
|
| 35 |
+
self.n_heads = n_heads
|
| 36 |
+
self.d_head = d_model // n_heads
|
| 37 |
+
self.max_dist = max_dist
|
| 38 |
+
|
| 39 |
+
self.w_q = nn.Linear(d_model, d_model)
|
| 40 |
+
self.w_k = nn.Linear(d_model, d_model)
|
| 41 |
+
self.w_v = nn.Linear(d_model, d_model)
|
| 42 |
+
self.w_o = nn.Linear(d_model, d_model)
|
| 43 |
+
|
| 44 |
+
self.u_bias = nn.Parameter(torch.randn(self.n_heads, self.d_head))
|
| 45 |
+
self.v_bias = nn.Parameter(torch.randn(self.n_heads, self.d_head))
|
| 46 |
+
self.w_r = nn.Linear(d_model, d_model)
|
| 47 |
+
|
| 48 |
+
self.dropout = nn.Dropout(dropout)
|
| 49 |
+
self.layer_norm = nn.LayerNorm(d_model)
|
| 50 |
+
|
| 51 |
+
def forward(self, q, k, v, pos_emb, key_padding_mask=None, attn_mask=None, values_provided=False):
|
| 52 |
+
batch_size, seq_len_q, _ = q.size()
|
| 53 |
+
seq_len_k = k.size(1)
|
| 54 |
+
|
| 55 |
+
residual = q
|
| 56 |
+
|
| 57 |
+
q = self.w_q(q).view(batch_size, seq_len_q, self.n_heads, self.d_head)
|
| 58 |
+
k = self.w_k(k).view(batch_size, seq_len_k, self.n_heads, self.d_head)
|
| 59 |
+
v_transformed = self.w_v(v).view(batch_size, seq_len_k, self.n_heads, self.d_head)
|
| 60 |
+
|
| 61 |
+
q_with_u = (q + self.u_bias).transpose(1, 2) # (B, h, T_q, d_h)
|
| 62 |
+
q_with_v = (q + self.v_bias).transpose(1, 2) # (B, h, T_q, d_h)
|
| 63 |
+
k = k.transpose(1, 2) # (B, h, T_k, d_h)
|
| 64 |
+
v_transformed = v_transformed.transpose(1, 2) # (B, h, T_k, d_h)
|
| 65 |
+
|
| 66 |
+
pos_emb = self.w_r(pos_emb).view(seq_len_q, seq_len_k, self.n_heads, self.d_head)
|
| 67 |
+
pos_emb = pos_emb.permute(2, 0, 3, 1) # (h, T_q, d_h, T_k)
|
| 68 |
+
|
| 69 |
+
# Content-based addressing
|
| 70 |
+
ac = torch.matmul(q_with_u, k.transpose(-2, -1)) # (B, h, T_q, T_k)
|
| 71 |
+
|
| 72 |
+
# Position-based addressing
|
| 73 |
+
q_for_bd = q_with_v.permute(1, 2, 0, 3) # (h, T_q, B, d_h)
|
| 74 |
+
bd_t = torch.matmul(q_for_bd, pos_emb) # (h, T_q, B, T_k)
|
| 75 |
+
bd = bd_t.permute(2, 0, 1, 3) # (B, h, T_q, T_k)
|
| 76 |
+
|
| 77 |
+
attn_score = (ac + bd) / math.sqrt(self.d_head)
|
| 78 |
+
|
| 79 |
+
# Apply key and (optional) attention masks
|
| 80 |
+
if key_padding_mask is not None:
|
| 81 |
+
attn_score = attn_score.masked_fill(key_padding_mask.unsqueeze(1).unsqueeze(2), float('-inf'))
|
| 82 |
+
|
| 83 |
+
if attn_mask is not None:
|
| 84 |
+
attn_score = attn_score.masked_fill(attn_mask.unsqueeze(0), float('-inf'))
|
| 85 |
+
|
| 86 |
+
# ------------------------------------------------------------------
|
| 87 |
+
# Guard against rows where *all* keys were masked (e.g., an attention
|
| 88 |
+
# block that consists purely of padding tokens). A softmax over a
|
| 89 |
+
# vector of \(-\infty\) would otherwise produce NaNs. We detect such
|
| 90 |
+
# rows and set their scores to zero, ensuring a well-defined softmax
|
| 91 |
+
# (which will yield a uniform distribution) before later resetting
|
| 92 |
+
# their weights to zero.
|
| 93 |
+
# ------------------------------------------------------------------
|
| 94 |
+
all_masked = torch.isinf(attn_score) & (attn_score < 0) # True where -inf
|
| 95 |
+
all_masked = all_masked.all(dim=-1, keepdim=True) # (B, h, T_q, 1)
|
| 96 |
+
|
| 97 |
+
# For rows with all_masked == True, replace -inf with 0 so that the
|
| 98 |
+
# subsequent softmax does not generate NaNs.
|
| 99 |
+
attn_score = attn_score.masked_fill(all_masked, 0.0)
|
| 100 |
+
|
| 101 |
+
attn_weights = F.softmax(attn_score, dim=-1) # (B, h, T_q, T_k)
|
| 102 |
+
|
| 103 |
+
# After the softmax, zero-out rows that correspond to fully-padded
|
| 104 |
+
# queries so they don’t influence the output.
|
| 105 |
+
attn_weights = attn_weights.masked_fill(all_masked, 0.0)
|
| 106 |
+
attn_weights = self.dropout(attn_weights)
|
| 107 |
+
|
| 108 |
+
attn_output = torch.matmul(attn_weights, v_transformed) # (B, h, T_q, d_h)
|
| 109 |
+
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len_q, self.d_model)
|
| 110 |
+
|
| 111 |
+
attn_output = self.w_o(attn_output)
|
| 112 |
+
attn_output = self.dropout(attn_output)
|
| 113 |
+
|
| 114 |
+
if values_provided:
|
| 115 |
+
output = v + attn_output
|
| 116 |
+
else:
|
| 117 |
+
output = residual + attn_output
|
| 118 |
+
|
| 119 |
+
return self.layer_norm(output), attn_weights
|
| 120 |
+
|
| 121 |
+
class IntraBlockMHA(nn.Module):
|
| 122 |
+
def __init__(self, d_model, n_heads, dropout, max_dist=3):
|
| 123 |
+
super().__init__()
|
| 124 |
+
self.mha = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist=max_dist)
|
| 125 |
+
|
| 126 |
+
def forward(self, x, n_blocks, key_padding_mask=None):
|
| 127 |
+
batch_size, seq_len, _ = x.shape
|
| 128 |
+
block_len = seq_len // n_blocks
|
| 129 |
+
|
| 130 |
+
x = x.reshape(batch_size * n_blocks, block_len, -1)
|
| 131 |
+
|
| 132 |
+
mask = None
|
| 133 |
+
if key_padding_mask is not None:
|
| 134 |
+
mask = key_padding_mask.reshape(batch_size * n_blocks, block_len)
|
| 135 |
+
|
| 136 |
+
pos_emb = get_relative_position_encoding(block_len, x.size(-1), self.mha.max_dist).to(x.device)
|
| 137 |
+
|
| 138 |
+
x, _ = self.mha(x, x, x, pos_emb, key_padding_mask=mask)
|
| 139 |
+
return x.reshape(batch_size, seq_len, -1)
|
| 140 |
+
|
| 141 |
+
class ConvFFN(nn.Module):
|
| 142 |
+
def __init__(self, d_model, dropout=0.1):
|
| 143 |
+
super().__init__()
|
| 144 |
+
# piano roll resolution: 4 -> 12 (3x)
|
| 145 |
+
# kernel size: 3 -> 9 (3x)
|
| 146 |
+
self.conv1 = nn.Conv1d(d_model, d_model, kernel_size=9, padding=4)
|
| 147 |
+
self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=9, padding=4)
|
| 148 |
+
self.dropout = nn.Dropout(dropout)
|
| 149 |
+
self.norm = nn.LayerNorm(d_model)
|
| 150 |
+
|
| 151 |
+
def forward(self, x):
|
| 152 |
+
residual = x
|
| 153 |
+
x = x.transpose(1, 2)
|
| 154 |
+
x = F.relu(self.conv1(x))
|
| 155 |
+
x = self.dropout(x)
|
| 156 |
+
x = self.conv2(x)
|
| 157 |
+
x = self.dropout(x)
|
| 158 |
+
x = x.transpose(1, 2)
|
| 159 |
+
x = x + residual
|
| 160 |
+
return self.norm(x)
|
| 161 |
+
|
| 162 |
+
class TransformerLayer(nn.Module):
|
| 163 |
+
def __init__(self, d_model, n_heads, dropout, max_dist, use_conv_ffn=True):
|
| 164 |
+
super().__init__()
|
| 165 |
+
self.self_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
|
| 166 |
+
self.cross_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
|
| 167 |
+
self.pos_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
|
| 168 |
+
if use_conv_ffn:
|
| 169 |
+
self.ffn = ConvFFN(d_model, dropout)
|
| 170 |
+
else: # Fallback to original FFN if needed
|
| 171 |
+
self.ffn = nn.Sequential(
|
| 172 |
+
nn.Linear(d_model, d_model * 4),
|
| 173 |
+
nn.ReLU(),
|
| 174 |
+
nn.Dropout(dropout),
|
| 175 |
+
nn.Linear(d_model * 4, d_model),
|
| 176 |
+
nn.Dropout(dropout)
|
| 177 |
+
)
|
| 178 |
+
self.norm = nn.LayerNorm(d_model)
|
| 179 |
+
|
| 180 |
+
self.use_conv_ffn = use_conv_ffn
|
| 181 |
+
|
| 182 |
+
def forward(self, dec_input, enc_output, pos_emb, dec_pos_emb,
|
| 183 |
+
tgt_mask=None, memory_mask=None,
|
| 184 |
+
tgt_key_padding_mask=None, memory_key_padding_mask=None):
|
| 185 |
+
# Self-attention
|
| 186 |
+
dec_output, _ = self.self_attn(dec_input, dec_input, dec_input, pos_emb, key_padding_mask=tgt_key_padding_mask)
|
| 187 |
+
|
| 188 |
+
# Positional Attention
|
| 189 |
+
dec_output = self.pos_attn(dec_pos_emb.unsqueeze(0).repeat(dec_input.size(0), 1, 1),
|
| 190 |
+
dec_pos_emb.unsqueeze(0).repeat(dec_input.size(0), 1, 1),
|
| 191 |
+
dec_output, pos_emb=pos_emb,
|
| 192 |
+
key_padding_mask=tgt_key_padding_mask, values_provided=True)[0]
|
| 193 |
+
|
| 194 |
+
# Cross-attention
|
| 195 |
+
dec_output, _ = self.cross_attn(dec_output, enc_output, enc_output, pos_emb, key_padding_mask=memory_key_padding_mask)
|
| 196 |
+
|
| 197 |
+
# FFN
|
| 198 |
+
if self.use_conv_ffn:
|
| 199 |
+
dec_output = self.ffn(dec_output)
|
| 200 |
+
else:
|
| 201 |
+
dec_output = self.norm(dec_output + self.ffn(dec_output))
|
| 202 |
+
|
| 203 |
+
return dec_output
|
| 204 |
+
|
| 205 |
+
class PositionalEncoding(nn.Module):
|
| 206 |
+
"""Injects some information about the relative or absolute position of the tokens in the sequence."""
|
| 207 |
+
def __init__(self, d_model, dropout=0.1, max_len=5000):
|
| 208 |
+
super(PositionalEncoding, self).__init__()
|
| 209 |
+
self.dropout = nn.Dropout(p=dropout)
|
| 210 |
+
|
| 211 |
+
pe = torch.zeros(max_len, d_model)
|
| 212 |
+
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
| 213 |
+
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
| 214 |
+
pe[:, 0::2] = torch.sin(position * div_term)
|
| 215 |
+
pe[:, 1::2] = torch.cos(position * div_term)
|
| 216 |
+
pe = pe.unsqueeze(0)
|
| 217 |
+
self.register_buffer('pe', pe)
|
| 218 |
+
|
| 219 |
+
def forward(self, x):
|
| 220 |
+
"""
|
| 221 |
+
Args:
|
| 222 |
+
x: Tensor, shape [batch_size, seq_len, d_model]
|
| 223 |
+
"""
|
| 224 |
+
x = x + self.pe[:, :x.size(1), :]
|
| 225 |
+
return self.dropout(x)
|
| 226 |
+
|
| 227 |
+
class BinaryRound(torch.autograd.Function):
|
| 228 |
+
"""
|
| 229 |
+
Rounds a tensor whose values are in [0,1] to a tensor with values in {0, 1},
|
| 230 |
+
using the straight through estimator for the gradient.
|
| 231 |
+
"""
|
| 232 |
+
@staticmethod
|
| 233 |
+
def forward(ctx, input):
|
| 234 |
+
return torch.round(input).to(input.dtype)
|
| 235 |
+
|
| 236 |
+
@staticmethod
|
| 237 |
+
def backward(ctx, grad_output):
|
| 238 |
+
return grad_output
|
| 239 |
+
|
| 240 |
+
class HarmonyTransformer(nn.Module):
|
| 241 |
+
def __init__(self, config: Dict, vocab_sizes: Dict[str, int]):
|
| 242 |
+
super().__init__()
|
| 243 |
+
self.input_size = config['input_size']
|
| 244 |
+
self.d_model = config['d_model']
|
| 245 |
+
self.n_layers = config['n_layers']
|
| 246 |
+
self.n_heads = config['n_heads']
|
| 247 |
+
self.dropout_rate = config['dropout']
|
| 248 |
+
self.train_boundary = config['train_boundary']
|
| 249 |
+
self.slope = config.get('slope', 1.0)
|
| 250 |
+
self.max_len = config['n_beats'] * config['beat_resolution']
|
| 251 |
+
self.config = config
|
| 252 |
+
|
| 253 |
+
# Embeddings
|
| 254 |
+
self.enc_input_embed = nn.Linear(self.input_size, self.d_model)
|
| 255 |
+
self.dec_input_embed = nn.Linear(self.input_size, self.d_model)
|
| 256 |
+
|
| 257 |
+
self.pos_encoder = PositionalEncoding(self.d_model, self.dropout_rate, self.max_len)
|
| 258 |
+
self.register_buffer('pos_emb', get_relative_position_encoding(self.max_len, self.d_model, self.max_len - 1))
|
| 259 |
+
|
| 260 |
+
self.enc_intra_block_mha = IntraBlockMHA(self.d_model, self.n_heads, self.dropout_rate, max_dist=3)
|
| 261 |
+
self.dec_intra_block_mha = IntraBlockMHA(self.d_model, self.n_heads, self.dropout_rate, max_dist=3)
|
| 262 |
+
|
| 263 |
+
# Encoder
|
| 264 |
+
self.encoder_layers = nn.ModuleList([
|
| 265 |
+
RelativeMultiHeadAttention(self.d_model, self.n_heads, self.dropout_rate, self.max_len-1)
|
| 266 |
+
for _ in range(self.n_layers)
|
| 267 |
+
])
|
| 268 |
+
self.encoder_ffns = nn.ModuleList([ConvFFN(self.d_model, self.dropout_rate) for _ in range(self.n_layers)])
|
| 269 |
+
self.enc_weights = nn.Parameter(torch.zeros(self.n_layers + 1))
|
| 270 |
+
|
| 271 |
+
# Decoder
|
| 272 |
+
self.decoder_layers = nn.ModuleList([
|
| 273 |
+
TransformerLayer(self.d_model, self.n_heads, self.dropout_rate, self.max_len-1)
|
| 274 |
+
for _ in range(self.n_layers)
|
| 275 |
+
])
|
| 276 |
+
self.dec_weights = nn.Parameter(torch.zeros(self.n_layers + 1))
|
| 277 |
+
|
| 278 |
+
# Chord Change Prediction
|
| 279 |
+
self.chord_change_predictor = nn.Linear(self.d_model, 1)
|
| 280 |
+
|
| 281 |
+
# Output layers
|
| 282 |
+
self.root_predictor = nn.Linear(self.d_model, vocab_sizes['root'])
|
| 283 |
+
self.quality_predictor = nn.Linear(self.d_model, vocab_sizes['quality'])
|
| 284 |
+
self.bass_predictor = nn.Linear(self.d_model, vocab_sizes['bass'])
|
| 285 |
+
|
| 286 |
+
self._reset_parameters()
|
| 287 |
+
|
| 288 |
+
def _reset_parameters(self):
|
| 289 |
+
for p in self.parameters():
|
| 290 |
+
if p.dim() > 1:
|
| 291 |
+
nn.init.xavier_uniform_(p)
|
| 292 |
+
|
| 293 |
+
def chord_block_compression(self, hidden_states, chord_changes):
|
| 294 |
+
"""Compress hidden states according to chord changes."""
|
| 295 |
+
block_ids = torch.cumsum(chord_changes, dim=1) - chord_changes[:, 0].unsqueeze(1)
|
| 296 |
+
# Ensure integer dtype for one-hot encoding
|
| 297 |
+
block_ids = block_ids.long()
|
| 298 |
+
|
| 299 |
+
max_blocks = (torch.max(block_ids).item() + 1) if block_ids.numel() > 0 else 1
|
| 300 |
+
one_hot_ids = F.one_hot(block_ids, num_classes=max_blocks).float() # (B, S, M)
|
| 301 |
+
|
| 302 |
+
summed_states = torch.bmm(one_hot_ids.transpose(1, 2), hidden_states) # (B, M, H)
|
| 303 |
+
block_counts = one_hot_ids.sum(dim=1).unsqueeze(-1).clamp(min=1)
|
| 304 |
+
|
| 305 |
+
mean_states = summed_states / block_counts
|
| 306 |
+
# block_ids already of integer dtype
|
| 307 |
+
return mean_states, block_ids
|
| 308 |
+
|
| 309 |
+
def decode_compressed_sequences(self, compressed_sequences, block_ids):
|
| 310 |
+
"""Decode chord sequences according to chords_pred and block_ids."""
|
| 311 |
+
return torch.gather(compressed_sequences, 1, block_ids.unsqueeze(-1).expand(-1, -1, compressed_sequences.size(-1)))
|
| 312 |
+
|
| 313 |
+
def forward(self, src: torch.Tensor, src_key_padding_mask: torch.Tensor) -> Dict[str, torch.Tensor]:
|
| 314 |
+
"""
|
| 315 |
+
Args:
|
| 316 |
+
src: (batch_size, seq_len, input_size)
|
| 317 |
+
src_key_padding_mask: (batch_size, seq_len), True for valid, False for pad
|
| 318 |
+
Returns:
|
| 319 |
+
Dictionary of predictions.
|
| 320 |
+
"""
|
| 321 |
+
# --- Encoder ---
|
| 322 |
+
enc_output = self.pos_encoder(self.enc_input_embed(src))
|
| 323 |
+
|
| 324 |
+
# Intra-block MHA
|
| 325 |
+
enc_output = self.enc_intra_block_mha(enc_output, n_blocks=self.max_len//4, key_padding_mask=src_key_padding_mask)
|
| 326 |
+
|
| 327 |
+
# Main encoder layers
|
| 328 |
+
enc_layer_outputs = [enc_output]
|
| 329 |
+
for i in range(self.n_layers):
|
| 330 |
+
enc_output, _ = self.encoder_layers[i](enc_output, enc_output, enc_output,
|
| 331 |
+
self.pos_emb, key_padding_mask=src_key_padding_mask)
|
| 332 |
+
enc_output = self.encoder_ffns[i](enc_output)
|
| 333 |
+
enc_layer_outputs.append(enc_output)
|
| 334 |
+
|
| 335 |
+
# Weighted sum of encoder layers
|
| 336 |
+
enc_weights = F.softmax(self.enc_weights, dim=0)
|
| 337 |
+
enc_output = torch.stack(enc_layer_outputs, dim=-1) # (B, S, H, L+1)
|
| 338 |
+
enc_output = (enc_output * enc_weights).sum(dim=-1) # (B, S, H)
|
| 339 |
+
|
| 340 |
+
# --- Chord‐change prediction ---
|
| 341 |
+
boundary_logits = self.chord_change_predictor(enc_output) # (B, S, 1)
|
| 342 |
+
chord_change_prob = torch.sigmoid(self.slope * boundary_logits)
|
| 343 |
+
chord_change_pred = BinaryRound.apply(chord_change_prob).squeeze(-1) # (B, S) in {0,1}
|
| 344 |
+
|
| 345 |
+
# --- Decoder input embedding with regionalization ---
|
| 346 |
+
dec_input_embed = self.dec_input_embed(src)
|
| 347 |
+
dec_input_embed = F.dropout(dec_input_embed, p=self.dropout_rate, training=self.training)
|
| 348 |
+
dec_input_embed = self.dec_intra_block_mha(dec_input_embed, n_blocks=self.max_len // 4, key_padding_mask=src_key_padding_mask)
|
| 349 |
+
|
| 350 |
+
# Compress by predicted chord boundaries and expand back
|
| 351 |
+
dec_input_embed_reg, block_ids = self.chord_block_compression(dec_input_embed, chord_change_pred.long())
|
| 352 |
+
dec_input_embed_reg = self.decode_compressed_sequences(dec_input_embed_reg, block_ids)
|
| 353 |
+
|
| 354 |
+
# Combine embeddings
|
| 355 |
+
dec_input_embed = dec_input_embed + dec_input_embed_reg + enc_output
|
| 356 |
+
|
| 357 |
+
# Positional encoding
|
| 358 |
+
dec_input_embed = self.pos_encoder(dec_input_embed)
|
| 359 |
+
dec_pos_emb = self.pos_encoder.pe[:, :dec_input_embed.size(1), :].squeeze(0)
|
| 360 |
+
|
| 361 |
+
# --- Decoder layers with layer weighting ---
|
| 362 |
+
dec_layer_outputs = [dec_input_embed]
|
| 363 |
+
dec_output = dec_input_embed
|
| 364 |
+
for i in range(self.n_layers):
|
| 365 |
+
dec_output = self.decoder_layers[i](dec_output, enc_output, self.pos_emb, dec_pos_emb,
|
| 366 |
+
tgt_key_padding_mask=src_key_padding_mask,
|
| 367 |
+
memory_key_padding_mask=src_key_padding_mask)
|
| 368 |
+
dec_layer_outputs.append(dec_output)
|
| 369 |
+
|
| 370 |
+
dec_weights = F.softmax(self.dec_weights, dim=0)
|
| 371 |
+
dec_output = torch.stack(dec_layer_outputs, dim=-1) # (B, S, H, L+1)
|
| 372 |
+
dec_output = (dec_output * dec_weights).sum(dim=-1)
|
| 373 |
+
|
| 374 |
+
# --- Output Projections ---
|
| 375 |
+
root_logits = self.root_predictor(dec_output)
|
| 376 |
+
quality_logits = self.quality_predictor(dec_output)
|
| 377 |
+
bass_logits = self.bass_predictor(dec_output)
|
| 378 |
+
|
| 379 |
+
preds = {
|
| 380 |
+
'root': root_logits,
|
| 381 |
+
'quality': quality_logits,
|
| 382 |
+
'bass': bass_logits,
|
| 383 |
+
'boundary': boundary_logits
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
return preds
|
models/__init__.py
ADDED
|
File without changes
|
models/components.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Any, Dict, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
class PatchEmbedding(nn.Module):
|
| 9 |
+
def __init__(self, d_model: int, frames_per_patch: int = 6, expansion: int = 2):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.d_model = d_model
|
| 12 |
+
self.frames_per_patch = frames_per_patch
|
| 13 |
+
# Frame embedding (collapse pitch dim)
|
| 14 |
+
self.conv2d = nn.Conv2d(
|
| 15 |
+
in_channels=1,
|
| 16 |
+
out_channels=d_model,
|
| 17 |
+
kernel_size=(88, 1),
|
| 18 |
+
stride=(1, 1),
|
| 19 |
+
padding=(0, 0),
|
| 20 |
+
)
|
| 21 |
+
self.norm_frame = nn.LayerNorm(d_model)
|
| 22 |
+
# anti-aliasing conv on time axis
|
| 23 |
+
self.aa = nn.Conv1d(d_model, d_model, kernel_size=3, stride=1,
|
| 24 |
+
padding=1, groups=d_model, bias=False)
|
| 25 |
+
|
| 26 |
+
# Late temporal pooling (downsample frames -> patches)
|
| 27 |
+
self.glu_conv = nn.Conv1d(
|
| 28 |
+
in_channels=d_model,
|
| 29 |
+
out_channels=d_model * expansion * 2,
|
| 30 |
+
kernel_size=frames_per_patch,
|
| 31 |
+
stride=frames_per_patch,
|
| 32 |
+
padding=0,
|
| 33 |
+
bias=True,
|
| 34 |
+
)
|
| 35 |
+
self.project = nn.Conv1d(
|
| 36 |
+
in_channels=d_model * expansion,
|
| 37 |
+
out_channels=d_model,
|
| 38 |
+
kernel_size=1,
|
| 39 |
+
)
|
| 40 |
+
self.norm_temporal = nn.LayerNorm(d_model)
|
| 41 |
+
|
| 42 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 43 |
+
# x: (B, 1, 88, T)
|
| 44 |
+
x = self.conv2d(x) # (B, C, 1, T)
|
| 45 |
+
x = x.squeeze(2).transpose(1, 2) # (B, T, C)
|
| 46 |
+
x = self.norm_frame(x)
|
| 47 |
+
|
| 48 |
+
# anti-aliased and temporal pooling
|
| 49 |
+
x = x.transpose(1, 2) # (B, C, T)
|
| 50 |
+
x = self.aa(x) # (B, C, T)
|
| 51 |
+
v, g = self.glu_conv(x).chunk(2, dim=1)
|
| 52 |
+
x = self.project(v * torch.sigmoid(g)) # (B, C, T//k)
|
| 53 |
+
x = x.transpose(1, 2) # (B, T//k, C)
|
| 54 |
+
return self.norm_temporal(x)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def downsample_key_padding_mask(mask: Optional[torch.Tensor], frames_per_patch: int) -> Optional[torch.Tensor]:
|
| 58 |
+
if mask is None:
|
| 59 |
+
return None
|
| 60 |
+
bsz, total_len = mask.shape
|
| 61 |
+
if total_len < frames_per_patch:
|
| 62 |
+
return mask.new_zeros((bsz, 0), dtype=mask.dtype)
|
| 63 |
+
out_len = total_len // frames_per_patch
|
| 64 |
+
trimmed = mask[:, : out_len * frames_per_patch]
|
| 65 |
+
grouped = trimmed.view(bsz, out_len, frames_per_patch)
|
| 66 |
+
return grouped.all(dim=-1)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class RelativePositionBias(nn.Module):
|
| 70 |
+
def __init__(self, num_heads: int, max_distance: int) -> None:
|
| 71 |
+
super().__init__()
|
| 72 |
+
if max_distance < 1:
|
| 73 |
+
raise ValueError("max_distance must be >= 1")
|
| 74 |
+
self.num_heads = num_heads
|
| 75 |
+
self.max_distance = max_distance
|
| 76 |
+
self.bias = nn.Parameter(torch.zeros(2 * max_distance - 1, num_heads))
|
| 77 |
+
|
| 78 |
+
def forward(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
| 79 |
+
pos = torch.arange(seq_len, device=device)
|
| 80 |
+
rel = pos[:, None] - pos[None, :]
|
| 81 |
+
rel = rel.clamp(-self.max_distance + 1, self.max_distance - 1)
|
| 82 |
+
rel = rel + self.max_distance - 1
|
| 83 |
+
bias = self.bias[rel]
|
| 84 |
+
return bias.permute(2, 0, 1).to(dtype=dtype)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class RelativeTransformerEncoderLayer(nn.Module):
|
| 88 |
+
def __init__(
|
| 89 |
+
self,
|
| 90 |
+
d_model: int,
|
| 91 |
+
nhead: int,
|
| 92 |
+
dim_feedforward: int,
|
| 93 |
+
dropout: float,
|
| 94 |
+
activation: str,
|
| 95 |
+
) -> None:
|
| 96 |
+
super().__init__()
|
| 97 |
+
if d_model % nhead != 0:
|
| 98 |
+
raise ValueError("d_model must be divisible by nhead")
|
| 99 |
+
self.d_model = d_model
|
| 100 |
+
self.nhead = nhead
|
| 101 |
+
self.head_dim = d_model // nhead
|
| 102 |
+
|
| 103 |
+
self.qkv_proj = nn.Linear(d_model, 3 * d_model)
|
| 104 |
+
self.attn_dropout = nn.Dropout(dropout)
|
| 105 |
+
self.out_proj = nn.Linear(d_model, d_model)
|
| 106 |
+
self.resid_dropout = nn.Dropout(dropout)
|
| 107 |
+
|
| 108 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 109 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 110 |
+
|
| 111 |
+
self.linear1 = nn.Linear(d_model, dim_feedforward)
|
| 112 |
+
self.linear2 = nn.Linear(dim_feedforward, d_model)
|
| 113 |
+
self.ff_dropout = nn.Dropout(dropout)
|
| 114 |
+
|
| 115 |
+
if activation == "gelu":
|
| 116 |
+
self.activation_fn = F.gelu
|
| 117 |
+
elif activation == "relu":
|
| 118 |
+
self.activation_fn = F.relu
|
| 119 |
+
else:
|
| 120 |
+
raise ValueError(f"unsupported activation: {activation}")
|
| 121 |
+
|
| 122 |
+
def forward(
|
| 123 |
+
self,
|
| 124 |
+
src: torch.Tensor,
|
| 125 |
+
src_mask: Optional[torch.Tensor] = None,
|
| 126 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 127 |
+
attn_bias: Optional[torch.Tensor] = None,
|
| 128 |
+
) -> torch.Tensor:
|
| 129 |
+
bsz, seq_len, _ = src.size()
|
| 130 |
+
qkv = self.qkv_proj(src)
|
| 131 |
+
q, k, v = qkv.chunk(3, dim=-1)
|
| 132 |
+
q = q.view(bsz, seq_len, self.nhead, self.head_dim)
|
| 133 |
+
k = k.view(bsz, seq_len, self.nhead, self.head_dim)
|
| 134 |
+
v = v.view(bsz, seq_len, self.nhead, self.head_dim)
|
| 135 |
+
|
| 136 |
+
attn_scores = torch.einsum("bthd,bshd->bhts", q, k) / math.sqrt(self.head_dim)
|
| 137 |
+
|
| 138 |
+
if src_mask is not None:
|
| 139 |
+
if src_mask.dtype == torch.bool:
|
| 140 |
+
attn_scores = attn_scores.masked_fill(src_mask.unsqueeze(0), float("-inf"))
|
| 141 |
+
else:
|
| 142 |
+
attn_scores = attn_scores + src_mask.unsqueeze(0)
|
| 143 |
+
|
| 144 |
+
if src_key_padding_mask is not None:
|
| 145 |
+
key_mask = src_key_padding_mask.unsqueeze(1).unsqueeze(2)
|
| 146 |
+
attn_scores = attn_scores.masked_fill(key_mask, float("-inf"))
|
| 147 |
+
|
| 148 |
+
if attn_bias is not None:
|
| 149 |
+
if attn_bias.dim() == 3:
|
| 150 |
+
attn_scores = attn_scores + attn_bias.unsqueeze(0)
|
| 151 |
+
elif attn_bias.dim() == 4:
|
| 152 |
+
attn_scores = attn_scores + attn_bias
|
| 153 |
+
else:
|
| 154 |
+
raise ValueError("attn_bias must be 3D or 4D tensor")
|
| 155 |
+
|
| 156 |
+
attn_weights = F.softmax(attn_scores, dim=-1)
|
| 157 |
+
attn_weights = self.attn_dropout(attn_weights)
|
| 158 |
+
context = torch.einsum("bhts,bshd->bthd", attn_weights, v)
|
| 159 |
+
context = context.contiguous().view(bsz, seq_len, self.d_model)
|
| 160 |
+
attn_out = self.out_proj(context)
|
| 161 |
+
src = self.norm1(src + self.resid_dropout(attn_out))
|
| 162 |
+
|
| 163 |
+
ff_out = self.linear2(self.ff_dropout(self.activation_fn(self.linear1(src))))
|
| 164 |
+
src = self.norm2(src + self.resid_dropout(ff_out))
|
| 165 |
+
return src
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
class RelativeTransformerEncoder(nn.Module):
|
| 169 |
+
def __init__(
|
| 170 |
+
self,
|
| 171 |
+
num_layers: int,
|
| 172 |
+
d_model: int,
|
| 173 |
+
nhead: int,
|
| 174 |
+
dim_feedforward: int,
|
| 175 |
+
dropout: float,
|
| 176 |
+
activation: str,
|
| 177 |
+
relative_position_bias: Optional[RelativePositionBias],
|
| 178 |
+
) -> None:
|
| 179 |
+
super().__init__()
|
| 180 |
+
self.layers = nn.ModuleList(
|
| 181 |
+
[
|
| 182 |
+
RelativeTransformerEncoderLayer(
|
| 183 |
+
d_model=d_model,
|
| 184 |
+
nhead=nhead,
|
| 185 |
+
dim_feedforward=dim_feedforward,
|
| 186 |
+
dropout=dropout,
|
| 187 |
+
activation=activation,
|
| 188 |
+
)
|
| 189 |
+
for _ in range(num_layers)
|
| 190 |
+
]
|
| 191 |
+
)
|
| 192 |
+
self.norm = nn.LayerNorm(d_model)
|
| 193 |
+
self.rpb = relative_position_bias
|
| 194 |
+
|
| 195 |
+
def forward(
|
| 196 |
+
self,
|
| 197 |
+
src: torch.Tensor,
|
| 198 |
+
src_key_padding_mask: Optional[torch.Tensor],
|
| 199 |
+
) -> torch.Tensor:
|
| 200 |
+
output = src
|
| 201 |
+
if self.rpb is not None:
|
| 202 |
+
attn_bias = self.rpb(src.size(1), device=src.device, dtype=src.dtype)
|
| 203 |
+
else:
|
| 204 |
+
attn_bias = None
|
| 205 |
+
|
| 206 |
+
for layer in self.layers:
|
| 207 |
+
output = layer(
|
| 208 |
+
output,
|
| 209 |
+
src_key_padding_mask=src_key_padding_mask,
|
| 210 |
+
attn_bias=attn_bias,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
output = self.norm(output)
|
| 214 |
+
return output
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class ChordProjectionHead(nn.Module):
|
| 218 |
+
def __init__(self, d_model: int, vocab_sizes: Dict[str, int]) -> None:
|
| 219 |
+
super().__init__()
|
| 220 |
+
self.boundary_head = nn.Sequential(
|
| 221 |
+
nn.Linear(d_model, d_model // 2),
|
| 222 |
+
nn.GELU(),
|
| 223 |
+
nn.Linear(d_model // 2, 1),
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
projection_heads: Dict[str, nn.Module] = {}
|
| 227 |
+
for name, size in vocab_sizes.items():
|
| 228 |
+
projection_heads[name] = nn.Sequential(
|
| 229 |
+
nn.Linear(d_model, d_model // 2),
|
| 230 |
+
nn.GELU(),
|
| 231 |
+
nn.Linear(d_model // 2, size),
|
| 232 |
+
)
|
| 233 |
+
self.projection_heads = nn.ModuleDict(projection_heads)
|
| 234 |
+
|
| 235 |
+
def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:
|
| 236 |
+
boundary_logits = self.boundary_head(x).squeeze(-1)
|
| 237 |
+
outputs: Dict[str, torch.Tensor] = {"boundary": boundary_logits}
|
| 238 |
+
for comp, head in self.projection_heads.items():
|
| 239 |
+
outputs[comp] = head(x)
|
| 240 |
+
return outputs
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class KTokenDecoderLayer(nn.Module):
|
| 244 |
+
def __init__(self, d_model: int, nhead: int, mlp_ratio: int, dropout: float) -> None:
|
| 245 |
+
super().__init__()
|
| 246 |
+
if d_model % nhead != 0:
|
| 247 |
+
raise ValueError("d_model must be divisible by nhead")
|
| 248 |
+
self.d_model = d_model
|
| 249 |
+
self.nhead = nhead
|
| 250 |
+
self.head_dim = d_model // nhead
|
| 251 |
+
|
| 252 |
+
self.sa_qkv = nn.Linear(d_model, 3 * d_model)
|
| 253 |
+
self.sa_out = nn.Linear(d_model, d_model)
|
| 254 |
+
self.sa_ln = nn.LayerNorm(d_model)
|
| 255 |
+
self.sa_drop = nn.Dropout(dropout)
|
| 256 |
+
|
| 257 |
+
self.ca_q = nn.Linear(d_model, d_model)
|
| 258 |
+
self.ca_kv = nn.Linear(d_model, 2 * d_model)
|
| 259 |
+
self.ca_out = nn.Linear(d_model, d_model)
|
| 260 |
+
self.ca_ln = nn.LayerNorm(d_model)
|
| 261 |
+
self.ca_drop = nn.Dropout(dropout)
|
| 262 |
+
|
| 263 |
+
hidden = d_model * mlp_ratio
|
| 264 |
+
self.ff_ln = nn.LayerNorm(d_model)
|
| 265 |
+
self.ff = nn.Sequential(
|
| 266 |
+
nn.Linear(d_model, hidden),
|
| 267 |
+
nn.GELU(),
|
| 268 |
+
nn.Dropout(dropout),
|
| 269 |
+
nn.Linear(hidden, d_model),
|
| 270 |
+
)
|
| 271 |
+
self.ff_drop = nn.Dropout(dropout)
|
| 272 |
+
|
| 273 |
+
def _attn(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
|
| 274 |
+
attn = torch.einsum("nlhd,nshd->nhls", q, k) / math.sqrt(q.size(-1))
|
| 275 |
+
attn = torch.softmax(attn, dim=-1)
|
| 276 |
+
ctx = torch.einsum("nhls,nshd->nlhd", attn, v)
|
| 277 |
+
ctx = ctx.contiguous().view(q.size(0), q.size(1), -1)
|
| 278 |
+
return ctx
|
| 279 |
+
|
| 280 |
+
def forward(self, x_tokens: torch.Tensor, context: torch.Tensor) -> torch.Tensor:
|
| 281 |
+
bsz, length, k_tokens, d_model = x_tokens.shape
|
| 282 |
+
c_len = context.size(2)
|
| 283 |
+
batch_tokens = bsz * length
|
| 284 |
+
|
| 285 |
+
x = x_tokens.view(batch_tokens, k_tokens, d_model)
|
| 286 |
+
c = context.view(batch_tokens, c_len, d_model)
|
| 287 |
+
|
| 288 |
+
x_norm = self.sa_ln(x)
|
| 289 |
+
qkv = self.sa_qkv(x_norm)
|
| 290 |
+
q, k, v = qkv.chunk(3, dim=-1)
|
| 291 |
+
q = q.view(batch_tokens, k_tokens, self.nhead, self.head_dim)
|
| 292 |
+
k = k.view(batch_tokens, k_tokens, self.nhead, self.head_dim)
|
| 293 |
+
v = v.view(batch_tokens, k_tokens, self.nhead, self.head_dim)
|
| 294 |
+
sa_ctx = self._attn(q, k, v)
|
| 295 |
+
x = x + self.sa_drop(self.sa_out(sa_ctx))
|
| 296 |
+
|
| 297 |
+
x_norm = self.ca_ln(x)
|
| 298 |
+
q = self.ca_q(x_norm).view(batch_tokens, k_tokens, self.nhead, self.head_dim)
|
| 299 |
+
kv = self.ca_kv(c)
|
| 300 |
+
k, v = kv.chunk(2, dim=-1)
|
| 301 |
+
k = k.view(batch_tokens, c_len, self.nhead, self.head_dim)
|
| 302 |
+
v = v.view(batch_tokens, c_len, self.nhead, self.head_dim)
|
| 303 |
+
ca_ctx = self._attn(q, k, v)
|
| 304 |
+
x = x + self.ca_drop(self.ca_out(ca_ctx))
|
| 305 |
+
|
| 306 |
+
x_norm = self.ff_ln(x)
|
| 307 |
+
x = x + self.ff_drop(self.ff(x_norm))
|
| 308 |
+
|
| 309 |
+
return x.view(bsz, length, k_tokens, d_model)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def build_rpb(config: Dict[str, Any]) -> RelativePositionBias:
|
| 313 |
+
return RelativePositionBias(
|
| 314 |
+
num_heads=config["n_head"],
|
| 315 |
+
max_distance=config["n_beats"] * config["label_resolution"],
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def build_encoder(config: Dict[str, Any]) -> RelativeTransformerEncoder:
|
| 320 |
+
rpb = build_rpb(config)
|
| 321 |
+
return RelativeTransformerEncoder(
|
| 322 |
+
num_layers=config["num_encoder_layers"],
|
| 323 |
+
d_model=config["d_model"],
|
| 324 |
+
nhead=config["n_head"],
|
| 325 |
+
dim_feedforward=config["dim_feedforward"],
|
| 326 |
+
dropout=config["dropout"],
|
| 327 |
+
activation="gelu",
|
| 328 |
+
relative_position_bias=rpb,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def build_patch_embedding(config: Dict[str, Any]) -> PatchEmbedding:
|
| 333 |
+
return PatchEmbedding(
|
| 334 |
+
d_model=config["d_model"],
|
| 335 |
+
frames_per_patch=config["frames_per_patch"],
|
| 336 |
+
expansion=2,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
|
models/model.py
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import math
|
| 5 |
+
from typing import Dict, Any, Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class PatchEmbedding(nn.Module):
|
| 9 |
+
def __init__(self, d_model: int, frames_per_patch: int = 6, expansion: int = 2):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.d_model = d_model
|
| 12 |
+
self.frames_per_patch = frames_per_patch
|
| 13 |
+
# Frame embedding (collapse pitch dim)
|
| 14 |
+
self.conv2d = nn.Conv2d(
|
| 15 |
+
in_channels=1,
|
| 16 |
+
out_channels=d_model,
|
| 17 |
+
kernel_size=(88, 1),
|
| 18 |
+
stride=(1, 1),
|
| 19 |
+
padding=(0, 0),
|
| 20 |
+
)
|
| 21 |
+
self.norm_frame = nn.LayerNorm(d_model)
|
| 22 |
+
# anti-aliasing conv on time axis
|
| 23 |
+
self.aa = nn.Conv1d(d_model, d_model, kernel_size=3, stride=1,
|
| 24 |
+
padding=1, groups=d_model, bias=False)
|
| 25 |
+
|
| 26 |
+
# Late temporal pooling (downsample frames -> patches)
|
| 27 |
+
self.glu_conv = nn.Conv1d(
|
| 28 |
+
in_channels=d_model,
|
| 29 |
+
out_channels=d_model * expansion * 2,
|
| 30 |
+
kernel_size=frames_per_patch,
|
| 31 |
+
stride=frames_per_patch,
|
| 32 |
+
padding=0,
|
| 33 |
+
bias=True,
|
| 34 |
+
)
|
| 35 |
+
self.project = nn.Conv1d(
|
| 36 |
+
in_channels=d_model * expansion,
|
| 37 |
+
out_channels=d_model,
|
| 38 |
+
kernel_size=1,
|
| 39 |
+
)
|
| 40 |
+
self.norm_temporal = nn.LayerNorm(d_model)
|
| 41 |
+
|
| 42 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 43 |
+
# x: (B, 1, 88, T)
|
| 44 |
+
x = self.conv2d(x) # (B, C, 1, T)
|
| 45 |
+
x = x.squeeze(2).transpose(1, 2) # (B, T, C)
|
| 46 |
+
x = self.norm_frame(x)
|
| 47 |
+
|
| 48 |
+
# anti-aliased and temporal pooling
|
| 49 |
+
x = x.transpose(1, 2) # (B, C, T)
|
| 50 |
+
x = self.aa(x) # (B, C, T)
|
| 51 |
+
v, g = self.glu_conv(x).chunk(2, dim=1)
|
| 52 |
+
x = self.project(v * torch.sigmoid(g)) # (B, C, T//k)
|
| 53 |
+
x = x.transpose(1, 2) # (B, T//k, C)
|
| 54 |
+
return self.norm_temporal(x)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class PositionalEncoding(nn.Module):
|
| 58 |
+
def __init__(self, d_model, dropout=0.1, max_len=5000):
|
| 59 |
+
super(PositionalEncoding, self).__init__()
|
| 60 |
+
self.dropout = nn.Dropout(p=dropout)
|
| 61 |
+
|
| 62 |
+
pe = torch.zeros(max_len, d_model)
|
| 63 |
+
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
| 64 |
+
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
| 65 |
+
pe[:, 0::2] = torch.sin(position * div_term)
|
| 66 |
+
pe[:, 1::2] = torch.cos(position * div_term)
|
| 67 |
+
pe = pe.unsqueeze(0).transpose(0, 1)
|
| 68 |
+
self.register_buffer('pe', pe)
|
| 69 |
+
|
| 70 |
+
def forward(self, x):
|
| 71 |
+
x = x.transpose(0, 1)
|
| 72 |
+
x = x + self.pe[:x.size(0), :]
|
| 73 |
+
x = self.dropout(x)
|
| 74 |
+
return x.transpose(0, 1)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class RelativeTransformerEncoderLayer(nn.Module):
|
| 78 |
+
def __init__(self, d_model: int, nhead: int, dim_feedforward: int, dropout: float, activation: str = 'gelu'):
|
| 79 |
+
super().__init__()
|
| 80 |
+
if d_model % nhead != 0:
|
| 81 |
+
raise ValueError("d_model must be divisible by nhead.")
|
| 82 |
+
self.d_model = d_model
|
| 83 |
+
self.nhead = nhead
|
| 84 |
+
self.head_dim = d_model // nhead
|
| 85 |
+
|
| 86 |
+
self.qkv_proj = nn.Linear(d_model, 3 * d_model)
|
| 87 |
+
self.attn_dropout = nn.Dropout(dropout)
|
| 88 |
+
self.out_proj = nn.Linear(d_model, d_model)
|
| 89 |
+
self.resid_dropout = nn.Dropout(dropout)
|
| 90 |
+
|
| 91 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 92 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 93 |
+
|
| 94 |
+
self.linear1 = nn.Linear(d_model, dim_feedforward)
|
| 95 |
+
self.linear2 = nn.Linear(dim_feedforward, d_model)
|
| 96 |
+
self.ff_dropout = nn.Dropout(dropout)
|
| 97 |
+
|
| 98 |
+
if activation == 'gelu':
|
| 99 |
+
self.activation_fn = F.gelu
|
| 100 |
+
elif activation == 'relu':
|
| 101 |
+
self.activation_fn = F.relu
|
| 102 |
+
else:
|
| 103 |
+
raise ValueError(f"Unsupported activation: {activation}")
|
| 104 |
+
|
| 105 |
+
def forward(
|
| 106 |
+
self,
|
| 107 |
+
src: torch.Tensor,
|
| 108 |
+
src_mask: Optional[torch.Tensor] = None,
|
| 109 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 110 |
+
attn_bias: Optional[torch.Tensor] = None,
|
| 111 |
+
) -> torch.Tensor:
|
| 112 |
+
# src: (B, T_new, C)
|
| 113 |
+
bsz, seq_len_new, _ = src.size()
|
| 114 |
+
|
| 115 |
+
qkv = self.qkv_proj(src) # (B, T_new, 3*C)
|
| 116 |
+
q, k, v = qkv.chunk(3, dim=-1)
|
| 117 |
+
q = q.view(bsz, seq_len_new, self.nhead, self.head_dim)
|
| 118 |
+
k_all = k.view(bsz, seq_len_new, self.nhead, self.head_dim)
|
| 119 |
+
v_all = v.view(bsz, seq_len_new, self.nhead, self.head_dim)
|
| 120 |
+
|
| 121 |
+
# Attention: queries are only for current tokens; keys include past+current
|
| 122 |
+
attn_scores = torch.einsum('bthd,bshd->bhts', q, k_all) / math.sqrt(self.head_dim) # (B, H, T_new, T_total)
|
| 123 |
+
|
| 124 |
+
# Additive or boolean mask over attention logits
|
| 125 |
+
if src_mask is not None:
|
| 126 |
+
if src_mask.dtype == torch.bool:
|
| 127 |
+
attn_scores = attn_scores.masked_fill(src_mask.unsqueeze(0), float('-inf'))
|
| 128 |
+
else:
|
| 129 |
+
attn_scores = attn_scores + src_mask.unsqueeze(0)
|
| 130 |
+
|
| 131 |
+
# Key padding mask
|
| 132 |
+
if src_key_padding_mask is not None:
|
| 133 |
+
key_mask = src_key_padding_mask.unsqueeze(1).unsqueeze(2) # (B,1,1,T)
|
| 134 |
+
attn_scores = attn_scores.masked_fill(key_mask, float('-inf'))
|
| 135 |
+
|
| 136 |
+
if attn_bias is not None:
|
| 137 |
+
# Support 3D (H, T, T) or 4D (B, H, T, T)
|
| 138 |
+
if attn_bias.dim() == 3:
|
| 139 |
+
attn_scores = attn_scores + attn_bias.unsqueeze(0)
|
| 140 |
+
elif attn_bias.dim() == 4:
|
| 141 |
+
attn_scores = attn_scores + attn_bias
|
| 142 |
+
else:
|
| 143 |
+
raise ValueError("attn_bias must be 3D or 4D tensor if provided")
|
| 144 |
+
|
| 145 |
+
attn_weights = F.softmax(attn_scores, dim=-1)
|
| 146 |
+
attn_weights = self.attn_dropout(attn_weights)
|
| 147 |
+
|
| 148 |
+
context = torch.einsum('bhts,bshd->bthd', attn_weights, v_all) # (B, T_new, H, D)
|
| 149 |
+
context = context.contiguous().view(bsz, seq_len_new, self.d_model) # (B, T_new, C)
|
| 150 |
+
attn_out = self.out_proj(context)
|
| 151 |
+
|
| 152 |
+
src = src + self.resid_dropout(attn_out)
|
| 153 |
+
src = self.norm1(src)
|
| 154 |
+
|
| 155 |
+
ff = self.linear2(self.ff_dropout(self.activation_fn(self.linear1(src))))
|
| 156 |
+
src = src + self.resid_dropout(ff)
|
| 157 |
+
src = self.norm2(src)
|
| 158 |
+
|
| 159 |
+
return src
|
| 160 |
+
|
| 161 |
+
def downsample_key_padding_mask(mask: torch.Tensor, frames_per_patch: int) -> torch.Tensor:
|
| 162 |
+
# mask: (B, T) where True denotes padding.
|
| 163 |
+
bsz, total_len = mask.shape
|
| 164 |
+
if total_len < frames_per_patch:
|
| 165 |
+
# No valid output tokens from temporal pooling
|
| 166 |
+
return mask.new_ones((bsz, 0), dtype=mask.dtype)
|
| 167 |
+
out_len = total_len // frames_per_patch
|
| 168 |
+
trimmed = mask[:, :out_len * frames_per_patch]
|
| 169 |
+
grouped = trimmed.view(bsz, out_len, frames_per_patch)
|
| 170 |
+
return grouped.all(dim=-1)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class RelativePositionBias(nn.Module):
|
| 174 |
+
def __init__(self, num_heads: int, max_distance: int):
|
| 175 |
+
super().__init__()
|
| 176 |
+
if max_distance < 1:
|
| 177 |
+
raise ValueError("max_distance must be >= 1")
|
| 178 |
+
self.max_distance = max_distance
|
| 179 |
+
self.num_heads = num_heads
|
| 180 |
+
# Table over relative distances in [-max_distance+1, max_distance-1]
|
| 181 |
+
self.bias = nn.Parameter(torch.zeros(2 * max_distance - 1, num_heads))
|
| 182 |
+
|
| 183 |
+
def forward(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
| 184 |
+
# Compute clipped relative position indices
|
| 185 |
+
pos = torch.arange(seq_len, device=device)
|
| 186 |
+
rel = pos[:, None] - pos[None, :] # (T, T)
|
| 187 |
+
rel = rel.clamp(-self.max_distance + 1, self.max_distance - 1)
|
| 188 |
+
rel = rel + self.max_distance - 1 # shift to [0, 2*max_distance-2]
|
| 189 |
+
bias = self.bias[rel] # (T, T, H)
|
| 190 |
+
return bias.permute(2, 0, 1).to(dtype=dtype) # (H, T, T)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class RelativeTransformerEncoder(nn.Module):
|
| 194 |
+
def __init__(self, num_layers: int, d_model: int, nhead: int, dim_feedforward: int,
|
| 195 |
+
dropout: float, activation: str = 'gelu', relative_position_bias = None):
|
| 196 |
+
super().__init__()
|
| 197 |
+
self.layers = nn.ModuleList([
|
| 198 |
+
RelativeTransformerEncoderLayer(
|
| 199 |
+
d_model=d_model,
|
| 200 |
+
nhead=nhead,
|
| 201 |
+
dim_feedforward=dim_feedforward,
|
| 202 |
+
dropout=dropout,
|
| 203 |
+
activation=activation,
|
| 204 |
+
) for _ in range(num_layers)
|
| 205 |
+
])
|
| 206 |
+
self.norm = nn.LayerNorm(d_model)
|
| 207 |
+
self.rpb = relative_position_bias
|
| 208 |
+
self.nhead = nhead
|
| 209 |
+
|
| 210 |
+
def forward(
|
| 211 |
+
self,
|
| 212 |
+
src: torch.Tensor,
|
| 213 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 214 |
+
) -> torch.Tensor:
|
| 215 |
+
output = src
|
| 216 |
+
|
| 217 |
+
if self.rpb is not None:
|
| 218 |
+
attn_bias = self.rpb(src.size(1), device=src.device, dtype=src.dtype)
|
| 219 |
+
else:
|
| 220 |
+
attn_bias = None
|
| 221 |
+
|
| 222 |
+
for mod in self.layers:
|
| 223 |
+
output = mod(
|
| 224 |
+
output,
|
| 225 |
+
src_key_padding_mask=src_key_padding_mask,
|
| 226 |
+
attn_bias=attn_bias,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
output = self.norm(output)
|
| 230 |
+
return output
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class ChordDecomposeProjection(nn.Module):
|
| 234 |
+
def __init__(self, d_model: int, vocab_sizes: Dict[str, int]):
|
| 235 |
+
super().__init__()
|
| 236 |
+
self.d_model = d_model
|
| 237 |
+
self.vocab_sizes = vocab_sizes
|
| 238 |
+
self.boundary_head = nn.Sequential(
|
| 239 |
+
nn.Linear(d_model, d_model // 2),
|
| 240 |
+
nn.GELU(),
|
| 241 |
+
nn.Linear(d_model // 2, 1),
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
self.projection_heads = nn.ModuleDict()
|
| 245 |
+
for comp, size in self.vocab_sizes.items():
|
| 246 |
+
self.projection_heads[comp] = nn.Sequential(
|
| 247 |
+
nn.Linear(d_model, d_model // 2),
|
| 248 |
+
nn.GELU(),
|
| 249 |
+
nn.Linear(d_model // 2, size),
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 253 |
+
boundary_logits = self.boundary_head(x)
|
| 254 |
+
|
| 255 |
+
output = {'boundary': boundary_logits.squeeze(-1)}
|
| 256 |
+
for comp, head in self.projection_heads.items():
|
| 257 |
+
output[comp] = head(x)
|
| 258 |
+
|
| 259 |
+
return output
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
class ChordRecognitionModel(nn.Module):
|
| 263 |
+
def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
|
| 264 |
+
super().__init__()
|
| 265 |
+
self.config = model_config
|
| 266 |
+
self.vocab_sizes = vocab_sizes
|
| 267 |
+
self.d_model = self.config['d_model']
|
| 268 |
+
# Encoder: shared patch embedding and relative transformer (unchanged)
|
| 269 |
+
self.embedding = PatchEmbedding(
|
| 270 |
+
d_model=self.d_model,
|
| 271 |
+
frames_per_patch=self.config['frames_per_patch'],
|
| 272 |
+
expansion=2,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
self.input_dropout = nn.Dropout(self.config['dropout'])
|
| 276 |
+
|
| 277 |
+
rpb = RelativePositionBias(
|
| 278 |
+
num_heads=self.config['n_head'],
|
| 279 |
+
max_distance=self.config['n_beats'] * self.config['label_resolution']
|
| 280 |
+
)
|
| 281 |
+
self.relative_transformer_encoder = RelativeTransformerEncoder(
|
| 282 |
+
num_layers=self.config['num_encoder_layers'],
|
| 283 |
+
d_model=self.d_model,
|
| 284 |
+
nhead=self.config['n_head'],
|
| 285 |
+
dim_feedforward=self.config['dim_feedforward'],
|
| 286 |
+
dropout=self.config['dropout'],
|
| 287 |
+
activation='gelu',
|
| 288 |
+
relative_position_bias=rpb,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
# Boundary head, smoother, and FiLM gating
|
| 292 |
+
d_b = max(1, self.d_model // 4)
|
| 293 |
+
k_b = int(self.config.get('boundary_kernel', 5))
|
| 294 |
+
self.boundary_head = nn.Linear(self.d_model, 1)
|
| 295 |
+
self.boundary_smoother = nn.Conv1d(
|
| 296 |
+
in_channels=1,
|
| 297 |
+
out_channels=1,
|
| 298 |
+
kernel_size=k_b,
|
| 299 |
+
padding=k_b // 2,
|
| 300 |
+
groups=1,
|
| 301 |
+
bias=True,
|
| 302 |
+
)
|
| 303 |
+
self.boundary_e0 = nn.Parameter(torch.zeros(d_b))
|
| 304 |
+
self.boundary_e1 = nn.Parameter(torch.randn(d_b) * 0.02)
|
| 305 |
+
# Optional key context (3-part setting). Infer size from vocab_sizes if provided
|
| 306 |
+
self.Vq = int(vocab_sizes.get('quality', 0))
|
| 307 |
+
self.Vr = int(vocab_sizes.get('root', 0))
|
| 308 |
+
self.Vb = int(vocab_sizes.get('bass', 0))
|
| 309 |
+
|
| 310 |
+
# FiLM layers take boundary embedding
|
| 311 |
+
self.film_ln_in = nn.LayerNorm(self.d_model + d_b)
|
| 312 |
+
self.film_ln_h = nn.LayerNorm(self.d_model)
|
| 313 |
+
self.film_mlp = nn.Linear(self.d_model + d_b, 2 * self.d_model)
|
| 314 |
+
|
| 315 |
+
# Triple-token decoder: embeddings and heads
|
| 316 |
+
self.mask_id_q = int(self.config.get('mask_id_q', self.Vq))
|
| 317 |
+
self.mask_id_r = int(self.config.get('mask_id_r', self.Vr))
|
| 318 |
+
self.mask_id_b = int(self.config.get('mask_id_b', self.Vb))
|
| 319 |
+
self.emb_q = nn.Embedding(self.Vq + 1, self.d_model)
|
| 320 |
+
self.emb_r = nn.Embedding(self.Vr + 1, self.d_model)
|
| 321 |
+
self.emb_b = nn.Embedding(self.Vb + 1, self.d_model)
|
| 322 |
+
|
| 323 |
+
dec_heads = int(self.config.get('dec_heads', 4))
|
| 324 |
+
dec_mlp_ratio = int(self.config.get('dec_mlp_ratio', 4))
|
| 325 |
+
dec_layers = int(self.config.get('dec_layers', 1))
|
| 326 |
+
dec_dropout = float(self.config.get('dec_dropout', 0.1))
|
| 327 |
+
self.window_radius = int(self.config.get('window_radius', 2))
|
| 328 |
+
self.decoder_layers = nn.ModuleList([
|
| 329 |
+
KTokenDecoderLayer(
|
| 330 |
+
d_model=self.d_model,
|
| 331 |
+
nhead=dec_heads,
|
| 332 |
+
mlp_ratio=dec_mlp_ratio,
|
| 333 |
+
dropout=dec_dropout,
|
| 334 |
+
) for _ in range(dec_layers)
|
| 335 |
+
])
|
| 336 |
+
self.dec_norm = nn.LayerNorm(self.d_model)
|
| 337 |
+
self.head_q = nn.Linear(self.d_model, self.Vq)
|
| 338 |
+
self.head_r = nn.Linear(self.d_model, self.Vr)
|
| 339 |
+
self.head_b = nn.Linear(self.d_model, self.Vb)
|
| 340 |
+
|
| 341 |
+
# Legacy decompose projection for compatibility when training in decompose mode
|
| 342 |
+
self.chord_decompose_projection = ChordDecomposeProjection(self.d_model, self.vocab_sizes)
|
| 343 |
+
|
| 344 |
+
def forward(self, encoder_input: torch.Tensor,
|
| 345 |
+
src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 346 |
+
H, _ = self._encode(encoder_input, src_key_padding_mask)
|
| 347 |
+
out = self.chord_decompose_projection(H)
|
| 348 |
+
return out
|
| 349 |
+
|
| 350 |
+
# ===== Utilities =====
|
| 351 |
+
def _encode(self, encoder_input: torch.Tensor,
|
| 352 |
+
src_key_padding_mask: Optional[torch.Tensor]) -> (torch.Tensor, torch.Tensor):
|
| 353 |
+
x = self.embedding(encoder_input)
|
| 354 |
+
mask_down = None
|
| 355 |
+
if src_key_padding_mask is not None:
|
| 356 |
+
mask_down = downsample_key_padding_mask(src_key_padding_mask, self.config['frames_per_patch'])
|
| 357 |
+
x = self.input_dropout(x)
|
| 358 |
+
H = self.relative_transformer_encoder(x, src_key_padding_mask=mask_down)
|
| 359 |
+
boundary_logits = self.boundary_head(H).squeeze(-1)
|
| 360 |
+
return H, boundary_logits
|
| 361 |
+
|
| 362 |
+
def _smooth_boundary(self, boundary_logits: torch.Tensor) -> torch.Tensor:
|
| 363 |
+
b = boundary_logits.unsqueeze(1)
|
| 364 |
+
smoothed = self.boundary_smoother(b)
|
| 365 |
+
return torch.sigmoid(smoothed.squeeze(1))
|
| 366 |
+
|
| 367 |
+
def _apply_film(self, H: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
|
| 368 |
+
B, T, D = H.shape
|
| 369 |
+
e0 = self.boundary_e0.view(1, 1, -1).expand(B, T, -1)
|
| 370 |
+
e1 = self.boundary_e1.view(1, 1, -1).expand(B, T, -1)
|
| 371 |
+
# soft embedding for boundary
|
| 372 |
+
eb = b_soft.unsqueeze(-1) * e1 + (1.0 - b_soft).unsqueeze(-1) * e0
|
| 373 |
+
film_in = torch.cat([H, eb], dim=-1)
|
| 374 |
+
|
| 375 |
+
# layer norm and linear projection
|
| 376 |
+
film_in = self.film_ln_in(film_in)
|
| 377 |
+
gamma, beta = self.film_mlp(film_in).chunk(2, dim=-1)
|
| 378 |
+
Z = self.film_ln_h(H) * (1.0 + gamma) + beta
|
| 379 |
+
return Z
|
| 380 |
+
|
| 381 |
+
def _build_local_windows(self, H: torch.Tensor, radius: int) -> torch.Tensor:
|
| 382 |
+
x = H.transpose(1, 2)
|
| 383 |
+
padded = F.pad(x, (radius, radius), mode='replicate')
|
| 384 |
+
win = padded.unfold(dimension=2, size=2 * radius + 1, step=1)
|
| 385 |
+
win = win.permute(0, 2, 3, 1).contiguous()
|
| 386 |
+
return win
|
| 387 |
+
|
| 388 |
+
def _build_context(self, H: torch.Tensor, Z: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
|
| 389 |
+
local = self._build_local_windows(H, self.window_radius)
|
| 390 |
+
z = Z.unsqueeze(2)
|
| 391 |
+
parts = [z, local]
|
| 392 |
+
C = torch.cat(parts, dim=2)
|
| 393 |
+
return C
|
| 394 |
+
|
| 395 |
+
def _embed_tokens(self, ids_q: torch.Tensor, ids_r: torch.Tensor, ids_b: torch.Tensor) -> torch.Tensor:
|
| 396 |
+
xq = self.emb_q(ids_q)
|
| 397 |
+
xr = self.emb_r(ids_r)
|
| 398 |
+
xb = self.emb_b(ids_b)
|
| 399 |
+
X = torch.stack([xq, xr, xb], dim=2)
|
| 400 |
+
return X
|
| 401 |
+
|
| 402 |
+
def _run_decoder(self, X: torch.Tensor, C: torch.Tensor):
|
| 403 |
+
x = X
|
| 404 |
+
for layer in self.decoder_layers:
|
| 405 |
+
x = layer(x, C)
|
| 406 |
+
x = self.dec_norm(x)
|
| 407 |
+
xq = x[:, :, 0, :]
|
| 408 |
+
xr = x[:, :, 1, :]
|
| 409 |
+
xb = x[:, :, 2, :]
|
| 410 |
+
logits_q = self.head_q(xq)
|
| 411 |
+
logits_r = self.head_r(xr)
|
| 412 |
+
logits_b = self.head_b(xb)
|
| 413 |
+
return logits_q, logits_r, logits_b
|
| 414 |
+
|
| 415 |
+
# ===== Training forward =====
|
| 416 |
+
def forward_train(self, encoder_input: torch.Tensor,
|
| 417 |
+
targets: Dict[str, torch.Tensor],
|
| 418 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 419 |
+
target_mask: Optional[torch.Tensor] = None) -> Dict[str, Any]:
|
| 420 |
+
device = encoder_input.device
|
| 421 |
+
H, boundary_logits = self._encode(encoder_input, src_key_padding_mask)
|
| 422 |
+
# No key prediction/context in training
|
| 423 |
+
b_soft = self._smooth_boundary(boundary_logits)
|
| 424 |
+
Z = self._apply_film(H, b_soft)
|
| 425 |
+
C = self._build_context(H, Z, b_soft)
|
| 426 |
+
|
| 427 |
+
tgt_q = targets['quality']
|
| 428 |
+
tgt_r = targets['root']
|
| 429 |
+
tgt_b = targets['bass']
|
| 430 |
+
B, T = tgt_q.shape
|
| 431 |
+
if target_mask is None:
|
| 432 |
+
target_mask = torch.ones(B, T, dtype=torch.bool, device=device)
|
| 433 |
+
|
| 434 |
+
# mask n slots randomly per (B,T) across 3 slots [q,r,b]
|
| 435 |
+
k_rand = torch.randint(1, 4, (B, T), device=device)
|
| 436 |
+
rand_scores = torch.rand(B, T, 3, device=device)
|
| 437 |
+
top_vals, top_idx = torch.topk(rand_scores, k=3, dim=-1)
|
| 438 |
+
mask_slots = torch.zeros(B, T, 3, dtype=torch.bool, device=device)
|
| 439 |
+
# enable first k indices per position
|
| 440 |
+
for kk in range(1, 4):
|
| 441 |
+
sel = (k_rand == kk)
|
| 442 |
+
if sel.any():
|
| 443 |
+
idx_sel = top_idx[sel][:, :kk]
|
| 444 |
+
row = mask_slots[sel]
|
| 445 |
+
if idx_sel.numel() > 0:
|
| 446 |
+
row.scatter_(dim=1, index=idx_sel, value=True)
|
| 447 |
+
mask_slots[sel] = row
|
| 448 |
+
|
| 449 |
+
ids_q = tgt_q.clone()
|
| 450 |
+
ids_r = tgt_r.clone()
|
| 451 |
+
ids_b = tgt_b.clone()
|
| 452 |
+
ids_q[mask_slots[:, :, 0]] = self.mask_id_q
|
| 453 |
+
ids_r[mask_slots[:, :, 1]] = self.mask_id_r
|
| 454 |
+
ids_b[mask_slots[:, :, 2]] = self.mask_id_b
|
| 455 |
+
|
| 456 |
+
X = self._embed_tokens(ids_q, ids_r, ids_b)
|
| 457 |
+
logits_q, logits_r, logits_b = self._run_decoder(X, C)
|
| 458 |
+
|
| 459 |
+
def ce_masked(logits: Optional[torch.Tensor], target: torch.Tensor, slot_mask: torch.Tensor) -> torch.Tensor:
|
| 460 |
+
# Build supervision mask and safe targets to avoid CUDA asserts from out-of-range labels
|
| 461 |
+
m = slot_mask & target_mask # (B,T) supervised locations
|
| 462 |
+
num_classes = logits.size(-1)
|
| 463 |
+
safe_target = torch.where(
|
| 464 |
+
m,
|
| 465 |
+
target.clamp(min=0, max=num_classes - 1),
|
| 466 |
+
torch.zeros_like(target)
|
| 467 |
+
)
|
| 468 |
+
ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction='none')
|
| 469 |
+
denom = m.float().sum().clamp(min=1.0)
|
| 470 |
+
return (ce * m.float()).sum() / denom
|
| 471 |
+
|
| 472 |
+
loss_q = ce_masked(logits_q, tgt_q, mask_slots[:, :, 0])
|
| 473 |
+
loss_r = ce_masked(logits_r, tgt_r, mask_slots[:, :, 1])
|
| 474 |
+
loss_b = ce_masked(logits_b, tgt_b, mask_slots[:, :, 2])
|
| 475 |
+
|
| 476 |
+
bce = F.binary_cross_entropy_with_logits(boundary_logits,
|
| 477 |
+
targets['boundary'].to(boundary_logits.dtype),
|
| 478 |
+
pos_weight=torch.tensor(2.0, device=device),
|
| 479 |
+
reduction='none')
|
| 480 |
+
loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
|
| 481 |
+
total_loss = loss_q + loss_r + loss_b + loss_boundary * 3
|
| 482 |
+
|
| 483 |
+
with torch.no_grad():
|
| 484 |
+
stats = {}
|
| 485 |
+
for name, logits, target, m in [
|
| 486 |
+
('quality', logits_q, tgt_q, mask_slots[:, :, 0]),
|
| 487 |
+
('root', logits_r, tgt_r, mask_slots[:, :, 1]),
|
| 488 |
+
('bass', logits_b, tgt_b, mask_slots[:, :, 2]),
|
| 489 |
+
]:
|
| 490 |
+
if logits is None:
|
| 491 |
+
stats[f'acc_{name}'] = 0.0
|
| 492 |
+
stats[f'conf_{name}'] = 0.0
|
| 493 |
+
stats[f'ece_{name}'] = 0.0
|
| 494 |
+
else:
|
| 495 |
+
pred = logits.argmax(dim=-1)
|
| 496 |
+
sel = (m & target_mask)
|
| 497 |
+
denom = sel.float().sum().clamp(min=1.0)
|
| 498 |
+
acc = (pred[sel] == target[sel]).float().sum() / denom
|
| 499 |
+
prob = logits.float().softmax(dim=-1)
|
| 500 |
+
conf = prob.max(dim=-1).values
|
| 501 |
+
mean_conf = conf[sel].sum() / denom
|
| 502 |
+
# simple ECE
|
| 503 |
+
ece = torch.tensor(0.0, device=device)
|
| 504 |
+
bins = torch.linspace(0, 1, steps=11, device=device)
|
| 505 |
+
conf_flat = conf[sel]
|
| 506 |
+
pred_flat = pred[sel]
|
| 507 |
+
tgt_flat = target[sel]
|
| 508 |
+
for i in range(10):
|
| 509 |
+
lo, hi = bins[i], bins[i+1]
|
| 510 |
+
mask_bin = (conf_flat >= lo) & (conf_flat < hi if i < 9 else conf_flat <= hi)
|
| 511 |
+
if mask_bin.sum() > 0:
|
| 512 |
+
acc_bin = (pred_flat[mask_bin] == tgt_flat[mask_bin]).float().mean()
|
| 513 |
+
conf_bin = conf_flat[mask_bin].mean()
|
| 514 |
+
ece = ece + (mask_bin.float().mean() * (acc_bin - conf_bin).abs())
|
| 515 |
+
stats[f'acc_{name}'] = acc.item()
|
| 516 |
+
stats[f'conf_{name}'] = mean_conf.item()
|
| 517 |
+
stats[f'ece_{name}'] = ece.item()
|
| 518 |
+
|
| 519 |
+
return {
|
| 520 |
+
'loss': total_loss,
|
| 521 |
+
'loss_map': {
|
| 522 |
+
'quality': loss_q,
|
| 523 |
+
'root': loss_r,
|
| 524 |
+
'bass': loss_b,
|
| 525 |
+
'boundary': loss_boundary,
|
| 526 |
+
},
|
| 527 |
+
'logits': {
|
| 528 |
+
'quality': logits_q,
|
| 529 |
+
'root': logits_r,
|
| 530 |
+
'bass': logits_b,
|
| 531 |
+
},
|
| 532 |
+
'mask_slots': mask_slots, # (B,T,3) bool in order [q,r,b]
|
| 533 |
+
'boundary_logits': boundary_logits,
|
| 534 |
+
'stats': stats,
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
# ===== Inference forward =====
|
| 538 |
+
def forward_infer(self, encoder_input: torch.Tensor,
|
| 539 |
+
src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 540 |
+
device = encoder_input.device
|
| 541 |
+
H, boundary_logits = self._encode(encoder_input, src_key_padding_mask)
|
| 542 |
+
# No key prediction/context in inference
|
| 543 |
+
b_soft = self._smooth_boundary(boundary_logits)
|
| 544 |
+
Z = self._apply_film(H, b_soft)
|
| 545 |
+
C = self._build_context(H, Z, b_soft)
|
| 546 |
+
|
| 547 |
+
B, T, _ = H.shape
|
| 548 |
+
ids_q = torch.full((B, T), self.mask_id_q, dtype=torch.long, device=device)
|
| 549 |
+
ids_r = torch.full((B, T), self.mask_id_r, dtype=torch.long, device=device)
|
| 550 |
+
ids_b = torch.full((B, T), self.mask_id_b, dtype=torch.long, device=device)
|
| 551 |
+
filled_q = torch.zeros((B, T), dtype=torch.bool, device=device)
|
| 552 |
+
filled_r = torch.zeros((B, T), dtype=torch.bool, device=device)
|
| 553 |
+
filled_b = torch.zeros((B, T), dtype=torch.bool, device=device)
|
| 554 |
+
|
| 555 |
+
# Track decode order per time step: 0=quality, 1=root, 2=bass
|
| 556 |
+
decode_order = torch.full((B, T, 3), -1, dtype=torch.long, device=device)
|
| 557 |
+
order_pos = 0
|
| 558 |
+
|
| 559 |
+
for step in (3, 2, 1):
|
| 560 |
+
X = self._embed_tokens(ids_q, ids_r, ids_b)
|
| 561 |
+
logits_q, logits_r, logits_b = self._run_decoder(X, C)
|
| 562 |
+
pq = logits_q.softmax(dim=-1)
|
| 563 |
+
pr = logits_r.softmax(dim=-1)
|
| 564 |
+
pb = logits_b.softmax(dim=-1)
|
| 565 |
+
conf_q = pq.max(dim=-1).values
|
| 566 |
+
conf_r = pr.max(dim=-1).values
|
| 567 |
+
conf_b = pb.max(dim=-1).values
|
| 568 |
+
conf_q = conf_q.masked_fill(filled_q, float('-inf'))
|
| 569 |
+
conf_r = conf_r.masked_fill(filled_r, float('-inf'))
|
| 570 |
+
conf_b = conf_b.masked_fill(filled_b, float('-inf'))
|
| 571 |
+
conf = torch.stack([conf_q, conf_r, conf_b], dim=-1)
|
| 572 |
+
take_slot = conf.argmax(dim=-1)
|
| 573 |
+
|
| 574 |
+
# record order at this step
|
| 575 |
+
decode_order[:, :, order_pos] = take_slot
|
| 576 |
+
order_pos += 1
|
| 577 |
+
|
| 578 |
+
pred_q = logits_q.argmax(dim=-1)
|
| 579 |
+
commit_q = (take_slot == 0) | ((step == 1) & (~filled_q))
|
| 580 |
+
ids_q[commit_q] = pred_q[commit_q]
|
| 581 |
+
filled_q = filled_q | commit_q
|
| 582 |
+
|
| 583 |
+
pred_r = logits_r.argmax(dim=-1)
|
| 584 |
+
commit_r = (take_slot == 1) | ((step == 1) & (~filled_r))
|
| 585 |
+
ids_r[commit_r] = pred_r[commit_r]
|
| 586 |
+
filled_r = filled_r | commit_r
|
| 587 |
+
|
| 588 |
+
pred_b = logits_b.argmax(dim=-1)
|
| 589 |
+
commit_b = (take_slot == 2) | ((step == 1) & (~filled_b))
|
| 590 |
+
ids_b[commit_b] = pred_b[commit_b]
|
| 591 |
+
filled_b = filled_b | commit_b
|
| 592 |
+
|
| 593 |
+
# final confidences
|
| 594 |
+
X = self._embed_tokens(ids_q, ids_r, ids_b)
|
| 595 |
+
logits_q, logits_r, logits_b = self._run_decoder(X, C)
|
| 596 |
+
conf_q = logits_q.softmax(dim=-1).max(dim=-1).values
|
| 597 |
+
conf_r = logits_r.softmax(dim=-1).max(dim=-1).values
|
| 598 |
+
conf_b = logits_b.softmax(dim=-1).max(dim=-1).values
|
| 599 |
+
|
| 600 |
+
return {
|
| 601 |
+
'quality': ids_q,
|
| 602 |
+
'root': ids_r,
|
| 603 |
+
'bass': ids_b,
|
| 604 |
+
'conf_quality': conf_q,
|
| 605 |
+
'conf_root': conf_r,
|
| 606 |
+
'conf_bass': conf_b,
|
| 607 |
+
'boundary': boundary_logits,
|
| 608 |
+
'decode_order': decode_order,
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
class KTokenDecoderLayer(nn.Module):
|
| 613 |
+
def __init__(self, d_model: int, nhead: int, mlp_ratio: int, dropout: float):
|
| 614 |
+
super().__init__()
|
| 615 |
+
self.d_model = d_model
|
| 616 |
+
self.nhead = nhead
|
| 617 |
+
self.head_dim = d_model // nhead
|
| 618 |
+
if self.head_dim * nhead != d_model:
|
| 619 |
+
raise ValueError("d_model must be divisible by nhead")
|
| 620 |
+
|
| 621 |
+
# self-attention over K tokens
|
| 622 |
+
self.sa_qkv = nn.Linear(d_model, 3 * d_model)
|
| 623 |
+
self.sa_out = nn.Linear(d_model, d_model)
|
| 624 |
+
self.sa_ln = nn.LayerNorm(d_model)
|
| 625 |
+
self.sa_drop = nn.Dropout(dropout)
|
| 626 |
+
|
| 627 |
+
# cross-attention to context
|
| 628 |
+
self.ca_q = nn.Linear(d_model, d_model)
|
| 629 |
+
self.ca_kv = nn.Linear(d_model, 2 * d_model)
|
| 630 |
+
self.ca_out = nn.Linear(d_model, d_model)
|
| 631 |
+
self.ca_ln = nn.LayerNorm(d_model)
|
| 632 |
+
self.ca_drop = nn.Dropout(dropout)
|
| 633 |
+
|
| 634 |
+
# ffn
|
| 635 |
+
hidden = d_model * mlp_ratio
|
| 636 |
+
self.ff_ln = nn.LayerNorm(d_model)
|
| 637 |
+
self.ff = nn.Sequential(
|
| 638 |
+
nn.Linear(d_model, hidden),
|
| 639 |
+
nn.GELU(),
|
| 640 |
+
nn.Dropout(dropout),
|
| 641 |
+
nn.Linear(hidden, d_model),
|
| 642 |
+
)
|
| 643 |
+
self.ff_drop = nn.Dropout(dropout)
|
| 644 |
+
|
| 645 |
+
def _attn(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
|
| 646 |
+
# q,k,v: (N, L, H, D)
|
| 647 |
+
attn = torch.einsum('nlhd,nshd->nhls', q, k) / math.sqrt(q.size(-1)) # (N,H,L,S)
|
| 648 |
+
attn = torch.softmax(attn, dim=-1)
|
| 649 |
+
ctx = torch.einsum('nhls,nshd->nlhd', attn, v) # (N,L,H,D)
|
| 650 |
+
ctx = ctx.contiguous().view(q.size(0), q.size(1), -1) # (N,L,C)
|
| 651 |
+
return ctx
|
| 652 |
+
|
| 653 |
+
def forward(self, X: torch.Tensor, C: torch.Tensor) -> torch.Tensor:
|
| 654 |
+
# X: (B,T,K,D), C: (B,T,Lc,D)
|
| 655 |
+
B, T, K, D = X.shape
|
| 656 |
+
Lc = C.size(2)
|
| 657 |
+
N = B * T
|
| 658 |
+
# reshape
|
| 659 |
+
x = X.view(N, K, D)
|
| 660 |
+
c = C.view(N, Lc, D)
|
| 661 |
+
|
| 662 |
+
# self-attn (over K)
|
| 663 |
+
x_norm = self.sa_ln(x)
|
| 664 |
+
qkv = self.sa_qkv(x_norm)
|
| 665 |
+
q, k, v = qkv.chunk(3, dim=-1)
|
| 666 |
+
q = q.view(N, K, self.nhead, self.head_dim)
|
| 667 |
+
k = k.view(N, K, self.nhead, self.head_dim)
|
| 668 |
+
v = v.view(N, K, self.nhead, self.head_dim)
|
| 669 |
+
sa_ctx = self._attn(q, k, v) # (N,K,C)
|
| 670 |
+
x = x + self.sa_drop(self.sa_out(sa_ctx))
|
| 671 |
+
|
| 672 |
+
# cross-attn (queries = tokens, keys/values = context)
|
| 673 |
+
x_norm = self.ca_ln(x)
|
| 674 |
+
q = self.ca_q(x_norm).view(N, K, self.nhead, self.head_dim)
|
| 675 |
+
kv = self.ca_kv(c)
|
| 676 |
+
k, v = kv.chunk(2, dim=-1)
|
| 677 |
+
k = k.view(N, Lc, self.nhead, self.head_dim)
|
| 678 |
+
v = v.view(N, Lc, self.nhead, self.head_dim)
|
| 679 |
+
ca_ctx = self._attn(q, k, v) # (N,K,C)
|
| 680 |
+
x = x + self.ca_drop(self.ca_out(ca_ctx))
|
| 681 |
+
|
| 682 |
+
# ffn
|
| 683 |
+
x_norm = self.ff_ln(x)
|
| 684 |
+
x = x + self.ff_drop(self.ff(x_norm))
|
| 685 |
+
|
| 686 |
+
return x.view(B, T, K, D)
|
models/variants.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Dict, Any, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
from .components import (
|
| 9 |
+
downsample_key_padding_mask,
|
| 10 |
+
KTokenDecoderLayer,
|
| 11 |
+
ChordProjectionHead,
|
| 12 |
+
build_encoder,
|
| 13 |
+
build_patch_embedding,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _to_patch_input(x_bt88: torch.Tensor) -> torch.Tensor:
|
| 18 |
+
# Accepts (B, T, 88) -> returns (B, 1, 88, T)
|
| 19 |
+
return x_bt88.transpose(1, 2).unsqueeze(1).contiguous()
|
| 20 |
+
|
| 21 |
+
class BaseEncoder(nn.Module):
|
| 22 |
+
def __init__(self, model_config: Dict[str, Any]):
|
| 23 |
+
super().__init__()
|
| 24 |
+
self.config = model_config
|
| 25 |
+
self.d_model = self.config["d_model"]
|
| 26 |
+
self.embedding = build_patch_embedding(self.config)
|
| 27 |
+
self.input_dropout = nn.Dropout(self.config["dropout"])
|
| 28 |
+
self.encoder = build_encoder(self.config)
|
| 29 |
+
self.boundary_head = nn.Linear(self.d_model, 1)
|
| 30 |
+
|
| 31 |
+
def encode(self, encoder_input_bt88: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 32 |
+
x_pr = _to_patch_input(encoder_input_bt88)
|
| 33 |
+
x = self.embedding(x_pr)
|
| 34 |
+
mask_down = None
|
| 35 |
+
if src_key_padding_mask is not None:
|
| 36 |
+
mask_down = downsample_key_padding_mask(src_key_padding_mask, self.config["frames_per_patch"])
|
| 37 |
+
x = self.input_dropout(x)
|
| 38 |
+
h = self.encoder(x, src_key_padding_mask=mask_down)
|
| 39 |
+
boundary_logits = self.boundary_head(h).squeeze(-1)
|
| 40 |
+
return h, boundary_logits
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class BaselineLinearModel(nn.Module):
|
| 44 |
+
"""PatchEmbedding + TransformerEncoder + linear heads (baseline)."""
|
| 45 |
+
|
| 46 |
+
def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.config = model_config
|
| 49 |
+
self.encoder = BaseEncoder(model_config)
|
| 50 |
+
self.proj = ChordProjectionHead(model_config["d_model"], vocab_sizes)
|
| 51 |
+
self.use_key = ('key' in vocab_sizes)
|
| 52 |
+
|
| 53 |
+
def forward_train(
|
| 54 |
+
self,
|
| 55 |
+
encoder_input: torch.Tensor,
|
| 56 |
+
targets: Dict[str, torch.Tensor],
|
| 57 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 58 |
+
target_mask: Optional[torch.Tensor] = None,
|
| 59 |
+
vocabs: Optional[Dict[str, Any]] = None,
|
| 60 |
+
) -> Dict[str, Any]:
|
| 61 |
+
device = encoder_input.device
|
| 62 |
+
h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
|
| 63 |
+
outputs = self.proj(h)
|
| 64 |
+
|
| 65 |
+
bsz, t_len = targets["quality"].shape
|
| 66 |
+
if target_mask is None:
|
| 67 |
+
target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
|
| 68 |
+
|
| 69 |
+
def ce_masked(logits: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, comp_name: str) -> torch.Tensor:
|
| 70 |
+
num_classes = logits.size(-1)
|
| 71 |
+
comp_pad = vocabs.get(f"{comp_name}_pad_idx", vocabs["pad_idx"]) if vocabs is not None else 0
|
| 72 |
+
valid = mask & (target != comp_pad)
|
| 73 |
+
safe_target = torch.where(valid, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
|
| 74 |
+
ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
|
| 75 |
+
denom = valid.float().sum().clamp(min=1.0)
|
| 76 |
+
return (ce * valid.float()).sum() / denom
|
| 77 |
+
|
| 78 |
+
loss_q = ce_masked(outputs["quality"], targets["quality"], target_mask, "quality")
|
| 79 |
+
loss_r = ce_masked(outputs["root"], targets["root"], target_mask, "root")
|
| 80 |
+
loss_b = ce_masked(outputs["bass"], targets["bass"], target_mask, "bass")
|
| 81 |
+
if self.use_key and ("key" in outputs) and ("key" in targets):
|
| 82 |
+
loss_k = ce_masked(outputs["key"], targets["key"], target_mask, "key")
|
| 83 |
+
else:
|
| 84 |
+
loss_k = torch.tensor(0.0, device=device)
|
| 85 |
+
bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
|
| 86 |
+
loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
|
| 87 |
+
total_loss = loss_q + loss_r + loss_b + loss_k + loss_boundary * 3.0
|
| 88 |
+
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
logits = {k: v for k, v in outputs.items() if k in ("quality", "root", "bass")}
|
| 91 |
+
return {
|
| 92 |
+
"loss": total_loss,
|
| 93 |
+
"loss_map": {
|
| 94 |
+
"quality": loss_q,
|
| 95 |
+
"root": loss_r,
|
| 96 |
+
"bass": loss_b,
|
| 97 |
+
"key": loss_k,
|
| 98 |
+
"boundary": loss_boundary,
|
| 99 |
+
},
|
| 100 |
+
"logits": logits,
|
| 101 |
+
"boundary_logits": boundary_logits,
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 105 |
+
h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
|
| 106 |
+
outputs = self.proj(h)
|
| 107 |
+
comps = ["quality","root","bass"] + (["key"] if self.use_key and ("key" in outputs) else [])
|
| 108 |
+
ids = {k: outputs[k].argmax(dim=-1) for k in comps}
|
| 109 |
+
conf = {f"conf_{k}": outputs[k].softmax(dim=-1).max(dim=-1).values for k in comps}
|
| 110 |
+
ids.update(conf)
|
| 111 |
+
ids["boundary"] = boundary_logits
|
| 112 |
+
return ids
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class FiLMContextLinearModel(nn.Module):
|
| 116 |
+
"""FiLM injection + local context window + per-component context projector (linear heads)."""
|
| 117 |
+
|
| 118 |
+
def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
|
| 119 |
+
super().__init__()
|
| 120 |
+
self.config = model_config
|
| 121 |
+
self.encoder = BaseEncoder(model_config)
|
| 122 |
+
d_model = model_config["d_model"]
|
| 123 |
+
self.window_radius = int(model_config["window_radius"]) # Lc = 2R+1 + 1(Z)
|
| 124 |
+
|
| 125 |
+
# FiLM parameters (copy of CR-model style)
|
| 126 |
+
self.d_b = max(1, d_model // 4)
|
| 127 |
+
k_b = int(model_config["boundary_kernel"])
|
| 128 |
+
self.boundary_smoother = nn.Conv1d(1, 1, kernel_size=k_b, padding=k_b // 2, bias=True)
|
| 129 |
+
self.boundary_e0 = nn.Parameter(torch.zeros(self.d_b))
|
| 130 |
+
self.boundary_e1 = nn.Parameter(torch.randn(self.d_b) * 0.02)
|
| 131 |
+
|
| 132 |
+
self.film_ln_in = nn.LayerNorm(d_model + self.d_b)
|
| 133 |
+
self.film_ln_h = nn.LayerNorm(d_model)
|
| 134 |
+
self.film_mlp = nn.Linear(d_model + self.d_b, 2 * d_model)
|
| 135 |
+
|
| 136 |
+
# Per-component context heads
|
| 137 |
+
self.comp_names = ["quality", "root", "bass"]
|
| 138 |
+
if 'key' in vocab_sizes:
|
| 139 |
+
self.comp_names.append('key')
|
| 140 |
+
self.vocab_sizes = vocab_sizes
|
| 141 |
+
self.attn_mlp = nn.ModuleDict()
|
| 142 |
+
self.comp_proj = nn.ModuleDict()
|
| 143 |
+
for comp in self.comp_names:
|
| 144 |
+
self.attn_mlp[comp] = nn.Sequential(
|
| 145 |
+
nn.Linear(model_config["d_model"], model_config["d_model"] // 2),
|
| 146 |
+
nn.GELU(),
|
| 147 |
+
nn.Linear(model_config["d_model"] // 2, 1), # score per context token
|
| 148 |
+
)
|
| 149 |
+
self.comp_proj[comp] = nn.Sequential(
|
| 150 |
+
nn.Linear(model_config["d_model"], model_config["d_model"]),
|
| 151 |
+
nn.GELU(),
|
| 152 |
+
nn.Linear(model_config["d_model"], self.vocab_sizes[comp]),
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
def _smooth_boundary(self, boundary_logits: torch.Tensor) -> torch.Tensor:
|
| 156 |
+
b = boundary_logits.unsqueeze(1)
|
| 157 |
+
smoothed = self.boundary_smoother(b)
|
| 158 |
+
return torch.sigmoid(smoothed.squeeze(1))
|
| 159 |
+
|
| 160 |
+
def _apply_film(self, h: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
|
| 161 |
+
bsz, t_len, d_model = h.shape
|
| 162 |
+
e0 = self.boundary_e0.view(1, 1, -1).expand(bsz, t_len, -1)
|
| 163 |
+
e1 = self.boundary_e1.view(1, 1, -1).expand(bsz, t_len, -1)
|
| 164 |
+
eb = b_soft.unsqueeze(-1) * e1 + (1.0 - b_soft).unsqueeze(-1) * e0
|
| 165 |
+
film_in = torch.cat([h, eb], dim=-1)
|
| 166 |
+
film_in = self.film_ln_in(film_in)
|
| 167 |
+
gamma, beta = self.film_mlp(film_in).chunk(2, dim=-1)
|
| 168 |
+
z = self.film_ln_h(h) * (1.0 + gamma) + beta
|
| 169 |
+
return z
|
| 170 |
+
|
| 171 |
+
def _build_local_windows(self, h: torch.Tensor, radius: int) -> torch.Tensor:
|
| 172 |
+
x = h.transpose(1, 2)
|
| 173 |
+
padded = F.pad(x, (radius, radius), mode="replicate")
|
| 174 |
+
win = padded.unfold(dimension=2, size=2 * radius + 1, step=1)
|
| 175 |
+
win = win.permute(0, 2, 3, 1).contiguous()
|
| 176 |
+
return win
|
| 177 |
+
|
| 178 |
+
def _build_context(self, h: torch.Tensor, z: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
|
| 179 |
+
local = self._build_local_windows(h, self.window_radius)
|
| 180 |
+
z_exp = z.unsqueeze(2) # (B,T,1,D)
|
| 181 |
+
c = torch.cat([z_exp, local], dim=2) # (B,T,Lc,D)
|
| 182 |
+
return c
|
| 183 |
+
|
| 184 |
+
def _context_logits(self, c: torch.Tensor, comp: str) -> torch.Tensor:
|
| 185 |
+
# c: (B,T,L,D)
|
| 186 |
+
scores = self.attn_mlp[comp](c) # (B,T,L,1)
|
| 187 |
+
attn = torch.softmax(scores, dim=2)
|
| 188 |
+
pooled = (attn * c).sum(dim=2) # (B,T,D)
|
| 189 |
+
logits = self.comp_proj[comp](pooled) # (B,T,V)
|
| 190 |
+
return logits
|
| 191 |
+
|
| 192 |
+
def forward_train(
|
| 193 |
+
self,
|
| 194 |
+
encoder_input: torch.Tensor,
|
| 195 |
+
targets: Dict[str, torch.Tensor],
|
| 196 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 197 |
+
target_mask: Optional[torch.Tensor] = None,
|
| 198 |
+
vocabs: Optional[Dict[str, Any]] = None,
|
| 199 |
+
) -> Dict[str, Any]:
|
| 200 |
+
device = encoder_input.device
|
| 201 |
+
h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
|
| 202 |
+
b_soft = self._smooth_boundary(boundary_logits)
|
| 203 |
+
z = self._apply_film(h, b_soft)
|
| 204 |
+
c = self._build_context(h, z, b_soft)
|
| 205 |
+
|
| 206 |
+
logits = {comp: self._context_logits(c, comp) for comp in self.comp_names}
|
| 207 |
+
bsz, t_len = targets["quality"].shape
|
| 208 |
+
if target_mask is None:
|
| 209 |
+
target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
|
| 210 |
+
|
| 211 |
+
def ce_masked(logits: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, comp_name: str) -> torch.Tensor:
|
| 212 |
+
num_classes = logits.size(-1)
|
| 213 |
+
comp_pad = vocabs.get(f"{comp_name}_pad_idx", vocabs["pad_idx"]) if vocabs is not None else 0
|
| 214 |
+
valid = mask & (target != comp_pad)
|
| 215 |
+
safe_target = torch.where(valid, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
|
| 216 |
+
ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
|
| 217 |
+
denom = valid.float().sum().clamp(min=1.0)
|
| 218 |
+
return (ce * valid.float()).sum() / denom
|
| 219 |
+
|
| 220 |
+
loss_q = ce_masked(logits["quality"], targets["quality"], target_mask, "quality")
|
| 221 |
+
loss_r = ce_masked(logits["root"], targets["root"], target_mask, "root")
|
| 222 |
+
loss_b = ce_masked(logits["bass"], targets["bass"], target_mask, "bass")
|
| 223 |
+
loss_k = torch.tensor(0.0, device=device)
|
| 224 |
+
if 'key' in self.comp_names and ('key' in targets):
|
| 225 |
+
loss_k = ce_masked(logits['key'], targets['key'], target_mask, 'key')
|
| 226 |
+
bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
|
| 227 |
+
loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
|
| 228 |
+
total_loss = loss_q + loss_r + loss_b + loss_k + loss_boundary * 3.0
|
| 229 |
+
|
| 230 |
+
return {
|
| 231 |
+
"loss": total_loss,
|
| 232 |
+
"loss_map": {
|
| 233 |
+
"quality": loss_q,
|
| 234 |
+
"root": loss_r,
|
| 235 |
+
"bass": loss_b,
|
| 236 |
+
"key": loss_k,
|
| 237 |
+
"boundary": loss_boundary,
|
| 238 |
+
},
|
| 239 |
+
"logits": logits,
|
| 240 |
+
"boundary_logits": boundary_logits,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 244 |
+
h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
|
| 245 |
+
b_soft = self._smooth_boundary(boundary_logits)
|
| 246 |
+
z = self._apply_film(h, b_soft)
|
| 247 |
+
c = self._build_context(h, z, b_soft)
|
| 248 |
+
logits = {comp: self._context_logits(c, comp) for comp in self.comp_names}
|
| 249 |
+
ids = {k: logits[k].argmax(dim=-1) for k in self.comp_names}
|
| 250 |
+
conf = {f"conf_{k}": logits[k].softmax(dim=-1).max(dim=-1).values for k in self.comp_names}
|
| 251 |
+
ids.update(conf)
|
| 252 |
+
ids["boundary"] = boundary_logits
|
| 253 |
+
return ids
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class ChordRecognitionModelWrapper(nn.Module):
|
| 257 |
+
"""A thin wrapper around scripts.model.ChordRecognitionModel to normalize inputs."""
|
| 258 |
+
|
| 259 |
+
def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
|
| 260 |
+
super().__init__()
|
| 261 |
+
from .model import ChordRecognitionModel as CR
|
| 262 |
+
|
| 263 |
+
self.inner = CR(model_config, vocab_sizes)
|
| 264 |
+
|
| 265 |
+
def forward_train(
|
| 266 |
+
self,
|
| 267 |
+
encoder_input: torch.Tensor,
|
| 268 |
+
targets: Dict[str, torch.Tensor],
|
| 269 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 270 |
+
target_mask: Optional[torch.Tensor] = None,
|
| 271 |
+
vocabs: Optional[Dict[str, Any]] = None,
|
| 272 |
+
) -> Dict[str, Any]:
|
| 273 |
+
x = _to_patch_input(encoder_input)
|
| 274 |
+
return self.inner.forward_train(x, targets, src_key_padding_mask, target_mask)
|
| 275 |
+
|
| 276 |
+
def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 277 |
+
x = _to_patch_input(encoder_input)
|
| 278 |
+
return self.inner.forward_infer(x, src_key_padding_mask)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
class FiLMKTokenKeyModel(nn.Module):
|
| 282 |
+
"""FiLM + KToken decoder with key-conditioned FiLM injection (key used as auxiliary condition only)."""
|
| 283 |
+
|
| 284 |
+
def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int], key_vocab_size: int):
|
| 285 |
+
super().__init__()
|
| 286 |
+
self.config = model_config
|
| 287 |
+
self.vocab_sizes = vocab_sizes
|
| 288 |
+
d_model = model_config["d_model"]
|
| 289 |
+
self.encoder = BaseEncoder(model_config)
|
| 290 |
+
|
| 291 |
+
# FiLM
|
| 292 |
+
self.d_b = max(1, d_model // 4)
|
| 293 |
+
k_b = int(model_config["boundary_kernel"])
|
| 294 |
+
self.boundary_smoother = nn.Conv1d(1, 1, kernel_size=k_b, padding=k_b // 2, bias=True)
|
| 295 |
+
self.boundary_e0 = nn.Parameter(torch.zeros(self.d_b))
|
| 296 |
+
self.boundary_e1 = nn.Parameter(torch.randn(self.d_b) * 0.02)
|
| 297 |
+
|
| 298 |
+
# Key soft embedding
|
| 299 |
+
self.d_k = max(1, d_model // 8)
|
| 300 |
+
self.key_embed = nn.Embedding(key_vocab_size, self.d_k)
|
| 301 |
+
self.key_head = nn.Linear(d_model, key_vocab_size)
|
| 302 |
+
|
| 303 |
+
# FiLM projection with concatenated boundary/key embeddings
|
| 304 |
+
self.film_ln_in = nn.LayerNorm(d_model + self.d_b + self.d_k)
|
| 305 |
+
self.film_ln_h = nn.LayerNorm(d_model)
|
| 306 |
+
self.film_mlp = nn.Linear(d_model + self.d_b + self.d_k, 2 * d_model)
|
| 307 |
+
|
| 308 |
+
# Decoder (same as CR model)
|
| 309 |
+
self.mask_id_q = self.vocab_sizes["quality"]
|
| 310 |
+
self.mask_id_r = self.vocab_sizes["root"]
|
| 311 |
+
self.mask_id_b = self.vocab_sizes["bass"]
|
| 312 |
+
self.emb_q = nn.Embedding(self.vocab_sizes["quality"] + 1, d_model)
|
| 313 |
+
self.emb_r = nn.Embedding(self.vocab_sizes["root"] + 1, d_model)
|
| 314 |
+
self.emb_b = nn.Embedding(self.vocab_sizes["bass"] + 1, d_model)
|
| 315 |
+
|
| 316 |
+
dec_heads = int(model_config["dec_heads"])
|
| 317 |
+
dec_mlp_ratio = int(model_config["dec_mlp_ratio"])
|
| 318 |
+
dec_layers = int(model_config["dec_layers"])
|
| 319 |
+
dec_dropout = float(model_config["dec_dropout"])
|
| 320 |
+
self.window_radius = int(model_config["window_radius"])
|
| 321 |
+
self.decoder_layers = nn.ModuleList(
|
| 322 |
+
[
|
| 323 |
+
KTokenDecoderLayer(
|
| 324 |
+
d_model=d_model,
|
| 325 |
+
nhead=dec_heads,
|
| 326 |
+
mlp_ratio=dec_mlp_ratio,
|
| 327 |
+
dropout=dec_dropout,
|
| 328 |
+
)
|
| 329 |
+
for _ in range(dec_layers)
|
| 330 |
+
]
|
| 331 |
+
)
|
| 332 |
+
self.dec_norm = nn.LayerNorm(d_model)
|
| 333 |
+
self.head_q = nn.Linear(d_model, self.vocab_sizes["quality"])
|
| 334 |
+
self.head_r = nn.Linear(d_model, self.vocab_sizes["root"])
|
| 335 |
+
self.head_b = nn.Linear(d_model, self.vocab_sizes["bass"])
|
| 336 |
+
self.use_key = ("key" in self.vocab_sizes)
|
| 337 |
+
|
| 338 |
+
def _smooth_boundary(self, boundary_logits: torch.Tensor) -> torch.Tensor:
|
| 339 |
+
b = boundary_logits.unsqueeze(1)
|
| 340 |
+
smoothed = self.boundary_smoother(b)
|
| 341 |
+
return torch.sigmoid(smoothed.squeeze(1))
|
| 342 |
+
|
| 343 |
+
def _apply_film(self, h: torch.Tensor, b_soft: torch.Tensor, key_soft: torch.Tensor) -> torch.Tensor:
|
| 344 |
+
bsz, t_len, d_model = h.shape
|
| 345 |
+
# boundary embedding
|
| 346 |
+
e0 = self.boundary_e0.to(h.dtype).view(1, 1, -1).expand(bsz, t_len, -1)
|
| 347 |
+
e1 = self.boundary_e1.to(h.dtype).view(1, 1, -1).expand(bsz, t_len, -1)
|
| 348 |
+
eb = b_soft.unsqueeze(-1) * e1 + (1.0 - b_soft).unsqueeze(-1) * e0
|
| 349 |
+
# key soft embedding via expectation over embeddings
|
| 350 |
+
key_indices = torch.arange(self.key_embed.num_embeddings, device=h.device)
|
| 351 |
+
key_emb_table = self.key_embed(key_indices) # (V_k, d_k)
|
| 352 |
+
key_emb_table = key_emb_table.to(dtype=key_soft.dtype)
|
| 353 |
+
ek = key_soft @ key_emb_table # (B,T,d_k)
|
| 354 |
+
|
| 355 |
+
film_in = torch.cat([h, eb, ek], dim=-1)
|
| 356 |
+
film_in = self.film_ln_in(film_in)
|
| 357 |
+
gamma, beta = self.film_mlp(film_in).chunk(2, dim=-1)
|
| 358 |
+
z = self.film_ln_h(h) * (1.0 + gamma) + beta
|
| 359 |
+
return z
|
| 360 |
+
|
| 361 |
+
def _build_local_windows(self, h: torch.Tensor, radius: int) -> torch.Tensor:
|
| 362 |
+
x = h.transpose(1, 2)
|
| 363 |
+
padded = F.pad(x, (radius, radius), mode="replicate")
|
| 364 |
+
win = padded.unfold(dimension=2, size=2 * radius + 1, step=1)
|
| 365 |
+
win = win.permute(0, 2, 3, 1).contiguous()
|
| 366 |
+
return win
|
| 367 |
+
|
| 368 |
+
def _build_context(self, h: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
| 369 |
+
local = self._build_local_windows(h, self.window_radius)
|
| 370 |
+
z_exp = z.unsqueeze(2)
|
| 371 |
+
c = torch.cat([z_exp, local], dim=2)
|
| 372 |
+
return c
|
| 373 |
+
|
| 374 |
+
def _embed_tokens(self, ids_q: torch.Tensor, ids_r: torch.Tensor, ids_b: torch.Tensor) -> torch.Tensor:
|
| 375 |
+
xq = self.emb_q(ids_q)
|
| 376 |
+
xr = self.emb_r(ids_r)
|
| 377 |
+
xb = self.emb_b(ids_b)
|
| 378 |
+
x = torch.stack([xq, xr, xb], dim=2)
|
| 379 |
+
return x
|
| 380 |
+
|
| 381 |
+
def _run_decoder(self, X: torch.Tensor, C: torch.Tensor):
|
| 382 |
+
x = X
|
| 383 |
+
for layer in self.decoder_layers:
|
| 384 |
+
x = layer(x, C)
|
| 385 |
+
x = self.dec_norm(x)
|
| 386 |
+
xq = x[:, :, 0, :]
|
| 387 |
+
xr = x[:, :, 1, :]
|
| 388 |
+
xb = x[:, :, 2, :]
|
| 389 |
+
logits_q = self.head_q(xq)
|
| 390 |
+
logits_r = self.head_r(xr)
|
| 391 |
+
logits_b = self.head_b(xb)
|
| 392 |
+
return logits_q, logits_r, logits_b
|
| 393 |
+
|
| 394 |
+
def forward_train(
|
| 395 |
+
self,
|
| 396 |
+
encoder_input: torch.Tensor,
|
| 397 |
+
targets: Dict[str, torch.Tensor],
|
| 398 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 399 |
+
target_mask: Optional[torch.Tensor] = None,
|
| 400 |
+
vocabs: Optional[Dict[str, Any]] = None,
|
| 401 |
+
) -> Dict[str, Any]:
|
| 402 |
+
device = encoder_input.device
|
| 403 |
+
h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
|
| 404 |
+
b_soft = self._smooth_boundary(boundary_logits)
|
| 405 |
+
|
| 406 |
+
key_logits = self.key_head(h)
|
| 407 |
+
key_soft = key_logits.softmax(dim=-1)
|
| 408 |
+
|
| 409 |
+
z = self._apply_film(h, b_soft, key_soft)
|
| 410 |
+
C = self._build_context(h, z)
|
| 411 |
+
|
| 412 |
+
tgt_q = targets["quality"]
|
| 413 |
+
tgt_r = targets["root"]
|
| 414 |
+
tgt_b = targets["bass"]
|
| 415 |
+
bsz, t_len = tgt_q.shape
|
| 416 |
+
if target_mask is None:
|
| 417 |
+
target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
|
| 418 |
+
|
| 419 |
+
# Random mask-filling per CR model
|
| 420 |
+
k_rand = torch.randint(1, 4, (bsz, t_len), device=device)
|
| 421 |
+
rand_scores = torch.rand(bsz, t_len, 3, device=device)
|
| 422 |
+
top_vals, top_idx = torch.topk(rand_scores, k=3, dim=-1)
|
| 423 |
+
mask_slots = torch.zeros(bsz, t_len, 3, dtype=torch.bool, device=device)
|
| 424 |
+
for kk in range(1, 4):
|
| 425 |
+
sel = (k_rand == kk)
|
| 426 |
+
if sel.any():
|
| 427 |
+
idx_sel = top_idx[sel][:, :kk]
|
| 428 |
+
row = mask_slots[sel]
|
| 429 |
+
if idx_sel.numel() > 0:
|
| 430 |
+
row.scatter_(dim=1, index=idx_sel, value=True)
|
| 431 |
+
mask_slots[sel] = row
|
| 432 |
+
|
| 433 |
+
ids_q = tgt_q.clone()
|
| 434 |
+
ids_r = tgt_r.clone()
|
| 435 |
+
ids_b = tgt_b.clone()
|
| 436 |
+
ids_q[mask_slots[:, :, 0]] = self.mask_id_q
|
| 437 |
+
ids_r[mask_slots[:, :, 1]] = self.mask_id_r
|
| 438 |
+
ids_b[mask_slots[:, :, 2]] = self.mask_id_b
|
| 439 |
+
|
| 440 |
+
X = self._embed_tokens(ids_q, ids_r, ids_b)
|
| 441 |
+
logits_q, logits_r, logits_b = self._run_decoder(X, C)
|
| 442 |
+
|
| 443 |
+
def ce_masked(logits: torch.Tensor, target: torch.Tensor, slot_mask: torch.Tensor) -> torch.Tensor:
|
| 444 |
+
m = slot_mask & target_mask
|
| 445 |
+
num_classes = logits.size(-1)
|
| 446 |
+
safe_target = torch.where(m, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
|
| 447 |
+
ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
|
| 448 |
+
denom = m.float().sum().clamp(min=1.0)
|
| 449 |
+
return (ce * m.float()).sum() / denom
|
| 450 |
+
|
| 451 |
+
loss_q = ce_masked(logits_q, tgt_q, mask_slots[:, :, 0])
|
| 452 |
+
loss_r = ce_masked(logits_r, tgt_r, mask_slots[:, :, 1])
|
| 453 |
+
loss_b = ce_masked(logits_b, tgt_b, mask_slots[:, :, 2])
|
| 454 |
+
|
| 455 |
+
# Optional key cross-entropy (auxiliary, weight 1.0)
|
| 456 |
+
loss_k = torch.tensor(0.0, device=device)
|
| 457 |
+
if self.use_key and ("key" in targets):
|
| 458 |
+
key_logits = self.key_head(h)
|
| 459 |
+
num_classes_k = key_logits.size(-1)
|
| 460 |
+
safe_k = targets["key"].clamp(min=0, max=num_classes_k - 1)
|
| 461 |
+
loss_k = F.cross_entropy(key_logits.transpose(1, 2), safe_k, reduction="none")
|
| 462 |
+
denom_k = target_mask.float().sum().clamp(min=1.0)
|
| 463 |
+
loss_k = (loss_k * target_mask.float()).sum() / denom_k
|
| 464 |
+
|
| 465 |
+
bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
|
| 466 |
+
loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
|
| 467 |
+
total_loss = loss_q + loss_r + loss_b + loss_k + loss_boundary * 3.0
|
| 468 |
+
|
| 469 |
+
return {
|
| 470 |
+
"loss": total_loss,
|
| 471 |
+
"loss_map": {
|
| 472 |
+
"quality": loss_q,
|
| 473 |
+
"root": loss_r,
|
| 474 |
+
"bass": loss_b,
|
| 475 |
+
"key": loss_k,
|
| 476 |
+
"boundary": loss_boundary,
|
| 477 |
+
},
|
| 478 |
+
"logits": {
|
| 479 |
+
"quality": logits_q,
|
| 480 |
+
"root": logits_r,
|
| 481 |
+
"bass": logits_b,
|
| 482 |
+
},
|
| 483 |
+
"mask_slots": mask_slots,
|
| 484 |
+
"boundary_logits": boundary_logits,
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 488 |
+
device = encoder_input.device
|
| 489 |
+
h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
|
| 490 |
+
b_soft = self._smooth_boundary(boundary_logits)
|
| 491 |
+
key_logits = self.key_head(h)
|
| 492 |
+
key_soft = key_logits.softmax(dim=-1)
|
| 493 |
+
z = self._apply_film(h, b_soft, key_soft)
|
| 494 |
+
C = self._build_context(h, z)
|
| 495 |
+
|
| 496 |
+
bsz, t_len, _ = h.shape
|
| 497 |
+
ids_q = torch.full((bsz, t_len), self.mask_id_q, dtype=torch.long, device=device)
|
| 498 |
+
ids_r = torch.full((bsz, t_len), self.mask_id_r, dtype=torch.long, device=device)
|
| 499 |
+
ids_b = torch.full((bsz, t_len), self.mask_id_b, dtype=torch.long, device=device)
|
| 500 |
+
filled_q = torch.zeros((bsz, t_len), dtype=torch.bool, device=device)
|
| 501 |
+
filled_r = torch.zeros((bsz, t_len), dtype=torch.bool, device=device)
|
| 502 |
+
filled_b = torch.zeros((bsz, t_len), dtype=torch.bool, device=device)
|
| 503 |
+
|
| 504 |
+
for step in (3, 2, 1):
|
| 505 |
+
X = self._embed_tokens(ids_q, ids_r, ids_b)
|
| 506 |
+
logits_q, logits_r, logits_b = self._run_decoder(X, C)
|
| 507 |
+
pq = logits_q.softmax(dim=-1)
|
| 508 |
+
pr = logits_r.softmax(dim=-1)
|
| 509 |
+
pb = logits_b.softmax(dim=-1)
|
| 510 |
+
conf_q = pq.max(dim=-1).values
|
| 511 |
+
conf_r = pr.max(dim=-1).values
|
| 512 |
+
conf_b = pb.max(dim=-1).values
|
| 513 |
+
conf_q = conf_q.masked_fill(filled_q, float("-inf"))
|
| 514 |
+
conf_r = conf_r.masked_fill(filled_r, float("-inf"))
|
| 515 |
+
conf_b = conf_b.masked_fill(filled_b, float("-inf"))
|
| 516 |
+
conf = torch.stack([conf_q, conf_r, conf_b], dim=-1)
|
| 517 |
+
take_slot = conf.argmax(dim=-1)
|
| 518 |
+
|
| 519 |
+
pred_q = logits_q.argmax(dim=-1)
|
| 520 |
+
commit_q = (take_slot == 0) | ((step == 1) & (~filled_q))
|
| 521 |
+
ids_q[commit_q] = pred_q[commit_q]
|
| 522 |
+
filled_q = filled_q | commit_q
|
| 523 |
+
|
| 524 |
+
pred_r = logits_r.argmax(dim=-1)
|
| 525 |
+
commit_r = (take_slot == 1) | ((step == 1) & (~filled_r))
|
| 526 |
+
ids_r[commit_r] = pred_r[commit_r]
|
| 527 |
+
filled_r = filled_r | commit_r
|
| 528 |
+
|
| 529 |
+
pred_b = logits_b.argmax(dim=-1)
|
| 530 |
+
commit_b = (take_slot == 2) | ((step == 1) & (~filled_b))
|
| 531 |
+
ids_b[commit_b] = pred_b[commit_b]
|
| 532 |
+
filled_b = filled_b | commit_b
|
| 533 |
+
|
| 534 |
+
X = self._embed_tokens(ids_q, ids_r, ids_b)
|
| 535 |
+
logits_q, logits_r, logits_b = self._run_decoder(X, C)
|
| 536 |
+
conf_q = logits_q.softmax(dim=-1).max(dim=-1).values
|
| 537 |
+
conf_r = logits_r.softmax(dim=-1).max(dim=-1).values
|
| 538 |
+
conf_b = logits_b.softmax(dim=-1).max(dim=-1).values
|
| 539 |
+
return {
|
| 540 |
+
"quality": ids_q,
|
| 541 |
+
"root": ids_r,
|
| 542 |
+
"bass": ids_b,
|
| 543 |
+
"conf_quality": conf_q,
|
| 544 |
+
"conf_root": conf_r,
|
| 545 |
+
"conf_bass": conf_b,
|
| 546 |
+
"boundary": boundary_logits,
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
class HTAdapter(nn.Module):
|
| 551 |
+
"""Adapter to unify HT with the common training/eval interface."""
|
| 552 |
+
|
| 553 |
+
def __init__(self, ht_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
|
| 554 |
+
super().__init__()
|
| 555 |
+
from .HT import HarmonyTransformer
|
| 556 |
+
|
| 557 |
+
self.inner = HarmonyTransformer(ht_config, vocab_sizes)
|
| 558 |
+
self.vocab_sizes = vocab_sizes
|
| 559 |
+
|
| 560 |
+
def forward_train(
|
| 561 |
+
self,
|
| 562 |
+
encoder_input: torch.Tensor,
|
| 563 |
+
targets: Dict[str, torch.Tensor],
|
| 564 |
+
src_key_padding_mask: Optional[torch.Tensor] = None,
|
| 565 |
+
target_mask: Optional[torch.Tensor] = None,
|
| 566 |
+
vocabs: Optional[Dict[str, Any]] = None,
|
| 567 |
+
) -> Dict[str, Any]:
|
| 568 |
+
device = encoder_input.device
|
| 569 |
+
out = self.inner(encoder_input, src_key_padding_mask)
|
| 570 |
+
logits_q = out["quality"]
|
| 571 |
+
logits_r = out["root"]
|
| 572 |
+
logits_b = out["bass"]
|
| 573 |
+
boundary_logits = out["boundary"].squeeze(-1)
|
| 574 |
+
|
| 575 |
+
bsz, t_len, _ = logits_q.shape
|
| 576 |
+
if target_mask is None:
|
| 577 |
+
target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
|
| 578 |
+
|
| 579 |
+
def ce_masked(logits: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, comp_name: str) -> torch.Tensor:
|
| 580 |
+
num_classes = logits.size(-1)
|
| 581 |
+
comp_pad = vocabs.get(f"{comp_name}_pad_idx", vocabs["pad_idx"]) if vocabs is not None else 0
|
| 582 |
+
valid = mask & (target != comp_pad)
|
| 583 |
+
safe_target = torch.where(valid, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
|
| 584 |
+
ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
|
| 585 |
+
denom = valid.float().sum().clamp(min=1.0)
|
| 586 |
+
return (ce * valid.float()).sum() / denom
|
| 587 |
+
|
| 588 |
+
loss_q = ce_masked(logits_q, targets["quality"], target_mask, "quality")
|
| 589 |
+
loss_r = ce_masked(logits_r, targets["root"], target_mask, "root")
|
| 590 |
+
loss_b = ce_masked(logits_b, targets["bass"], target_mask, "bass")
|
| 591 |
+
bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
|
| 592 |
+
loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
|
| 593 |
+
total_loss = loss_q + loss_r + loss_b + 3.0 * loss_boundary
|
| 594 |
+
|
| 595 |
+
return {
|
| 596 |
+
"loss": total_loss,
|
| 597 |
+
"loss_map": {
|
| 598 |
+
"quality": loss_q,
|
| 599 |
+
"root": loss_r,
|
| 600 |
+
"bass": loss_b,
|
| 601 |
+
"boundary": loss_boundary,
|
| 602 |
+
},
|
| 603 |
+
"logits": {
|
| 604 |
+
"quality": logits_q,
|
| 605 |
+
"root": logits_r,
|
| 606 |
+
"bass": logits_b,
|
| 607 |
+
},
|
| 608 |
+
"boundary_logits": boundary_logits,
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
|
| 612 |
+
out = self.inner(encoder_input, src_key_padding_mask)
|
| 613 |
+
ids = {
|
| 614 |
+
"quality": out["quality"].argmax(dim=-1),
|
| 615 |
+
"root": out["root"].argmax(dim=-1),
|
| 616 |
+
"bass": out["bass"].argmax(dim=-1),
|
| 617 |
+
"boundary": out["boundary"].squeeze(-1),
|
| 618 |
+
}
|
| 619 |
+
return ids
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
def build_model(experiment: str, model_config: Dict[str, Any], vocabs: Dict[str, Any], use_key: bool = False) -> nn.Module:
|
| 623 |
+
# Build vocab sizes for components (include key optionally)
|
| 624 |
+
chord_components = ["root", "quality", "bass"] + (["key"] if use_key and ("key" in vocabs) else [])
|
| 625 |
+
vocab_sizes = {comp: len(vocabs[comp]) for comp in chord_components}
|
| 626 |
+
|
| 627 |
+
exp = experiment.lower()
|
| 628 |
+
if exp == "baseline":
|
| 629 |
+
return BaselineLinearModel(model_config, vocab_sizes)
|
| 630 |
+
if exp == "film_ctx":
|
| 631 |
+
return FiLMContextLinearModel(model_config, vocab_sizes)
|
| 632 |
+
if exp == "film_kdec":
|
| 633 |
+
return ChordRecognitionModelWrapper(model_config, vocab_sizes)
|
| 634 |
+
if exp == "film_kdec_key":
|
| 635 |
+
key_vocab_size = len(vocabs["key"]) if ("key" in vocabs) else 24
|
| 636 |
+
return FiLMKTokenKeyModel(model_config, vocab_sizes, key_vocab_size)
|
| 637 |
+
if exp == "ht":
|
| 638 |
+
# The HT config expects specific keys; reuse model_config where possible
|
| 639 |
+
ht_cfg = {
|
| 640 |
+
"input_size": model_config["input_size"],
|
| 641 |
+
"d_model": model_config["d_model"],
|
| 642 |
+
"n_layers": model_config["num_encoder_layers"],
|
| 643 |
+
"n_heads": model_config["n_head"],
|
| 644 |
+
"dropout": model_config["dropout"],
|
| 645 |
+
"train_boundary": True,
|
| 646 |
+
"slope": 1.0,
|
| 647 |
+
"n_beats": model_config["n_beats"],
|
| 648 |
+
"beat_resolution": model_config["beat_resolution"],
|
| 649 |
+
}
|
| 650 |
+
return HTAdapter(ht_cfg, vocab_sizes)
|
| 651 |
+
|
| 652 |
+
raise ValueError(f"Unknown experiment '{experiment}'")
|
| 653 |
+
|
| 654 |
+
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ffmpeg
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0
|
| 2 |
+
gradio==5.28.0
|
| 3 |
+
torch
|
| 4 |
+
torchaudio
|
| 5 |
+
numpy
|
| 6 |
+
pyyaml
|
| 7 |
+
music21
|
| 8 |
+
miditoolkit
|
| 9 |
+
huggingface_hub
|
| 10 |
+
tqdm
|