TheArtist Music Transformer — LoRA Adapter (Classical)
LoRA adapter (r=64) that conditions the F1 base (
PearlLeeStudio/TheArtist-MusicTransformer-ft-pop80) toward
classical chord progressions — Bach chorales (curated subset from the music21 corpus). One of eleven per-genre adapters from the paper
How Far Can Chord-Symbol Time-Series Adaptation Carry Genre Identity? (Lee, 2026). The released snapshot is the best rank of a 5-point sweep (r ∈ {4, 8, 16, 32, 64}); the vocabulary is extended 351 → 359 with
[GENRE:X] tokens shipped in
embedding_extension.pt.
Base-weights note. The released F1 base is weight-identical to the Phase-0 pop baseline (a checkpoint-selection artifact — see the note on the
base card). Every "F1 base" column below was measured against those exact weights, so the Δ shown is the adapter's gain over a pure-pop harmonic prior.
Usage
Requires torch, huggingface_hub, peft, safetensors. Both repos bundle model.py and tokenizer.py, so nothing needs to be cloned from GitHub.
1import sys
2import torch
3import torch.nn as nn
4from huggingface_hub import snapshot_download
5from peft import PeftModel
6
7# 1. Download the base + LoRA repos. Both bundle model.py and tokenizer.py.
8base_dir = snapshot_download(repo_id="PearlLeeStudio/TheArtist-MusicTransformer-ft-pop80")
9lora_dir = snapshot_download(repo_id="PearlLeeStudio/TheArtist-MusicTransformer-lora-classical")
10sys.path.insert(0, base_dir) # so the next two imports resolve
11
12from model import MusicTransformer
13from tokenizer import ChordTokenizer
14
15# 2. Extended tokenizer (351 base + 8 new genre tokens = 359). The PAD id
16# is unchanged across base and extended tokenizers.
17tokenizer = ChordTokenizer(include_extra_genres=True)
18
19# 3. Build the model at the BASE vocab size (351) so F1's state_dict loads
20# cleanly; we grow the embedding rows immediately after. Passing the
21# extended tokenizer's pad_id is safe because PAD is shared (see step 2).
22BASE_VOCAB = 351
23model = MusicTransformer(
24 vocab_size=BASE_VOCAB,
25 d_model=512, n_heads=8, d_ff=2048, n_layers=8,
26 max_seq_len=256, dropout=0.0, pad_id=tokenizer.pad_id,
27)
28ckpt = torch.load(f"{base_dir}/best.pt", map_location="cpu", weights_only=False)
29model.load_state_dict(ckpt["model_state_dict"])
30
31# 4. Grow token_emb + out_proj from 351 -> 359 (new rows init from
32# [GENRE:none]), then overlay the LoRA's trained extension rows.
33def _grow_to_extended_vocab(m, new_vocab, none_id):
34 d = m.token_emb.embedding_dim
35 new_emb = nn.Embedding(new_vocab, d, padding_idx=m.token_emb.padding_idx)
36 with torch.no_grad():
37 new_emb.weight[:m.token_emb.num_embeddings] = m.token_emb.weight
38 for i in range(m.token_emb.num_embeddings, new_vocab):
39 new_emb.weight[i] = m.token_emb.weight[none_id]
40 m.token_emb = new_emb
41 new_out = nn.Linear(d, new_vocab, bias=False)
42 with torch.no_grad():
43 new_out.weight[:m.out_proj.out_features] = m.out_proj.weight
44 for i in range(m.out_proj.out_features, new_vocab):
45 new_out.weight[i] = m.out_proj.weight[none_id]
46 m.out_proj = new_out
47
48_grow_to_extended_vocab(model, tokenizer.vocab_size, tokenizer.encode_genre("none"))
49
50ext = torch.load(f"{lora_dir}/embedding_extension.pt",
51 map_location="cpu", weights_only=False)
52model.token_emb.load_state_dict(ext["token_emb_state"])
53model.out_proj.load_state_dict(ext["out_proj_state"])
54
55# 5. Apply the LoRA adapter (the adapter files live at lora_dir/adapter/).
56model = PeftModel.from_pretrained(model, f"{lora_dir}/adapter")
57model.eval()
58
59# 6. Generate a classical continuation. With LoRA injected,
60# PeftModel.forward routes through the adapted attention layers.
61song = {
62 "key": "Cmaj", "time_signature": "4/4", "genre": "classical",
63 "bars": [["Cmaj7"], ["Fmaj7"]],
64}
65prompt_ids = tokenizer.encode_sequence(song)[:-1]
66ids = torch.tensor([prompt_ids])
67with torch.no_grad():
68 for _ in range(32):
69 logits = model(ids) # routed through LoRA via PeftModel
70 next_id = torch.multinomial(
71 torch.softmax(logits[:, -1, :] / 0.8, dim=-1), 1,
72 )
73 ids = torch.cat([ids, next_id], dim=-1)
74 if next_id.item() == tokenizer.eos_id:
75 break
76print(tokenizer.decode(ids[0].tolist()))
Evaluation
Teacher-forced token-level metrics on the classical val split (37 sequences, no key augmentation). Both columns use the same dataloader and the same [GENRE:none]-initialised embedding extension — only the adapter weights and trained embedding rows differ.
| Metric | F1 base | F1 + this LoRA (r=64) | Δ |
|---|
| Top-1 accuracy (%) | 43.54 | 60.55 | +17.01 |
| Top-5 accuracy (%) | 72.82 | 86.09 | +13.27 |
| Cross-entropy loss | 2.8653 | 1.3071 | -1.5582 |
Rank sweep the released adapter was selected from (minimum val loss, top-1 tiebreak):
| Rank | val_loss | Top-1 (%) | Δ Top-1 vs F1 |
|---|
| r=4 | 1.3663 | 58.15 | +14.61 |
| r=8 | 1.3486 | 58.74 | +15.20 |
| r=16 | 1.3333 | 59.61 | +16.07 |
| r=32 | 1.3174 | 60.08 | +16.54 |
| r=64 | 1.3071 | 60.55 | +17.01 ← released |
Real-song check — mean over 10 held-out classical songs (11–40 bars each; all 10 are named pieces (Bach chorales)):
| Model | Top-1 (%) | Top-5 (%) | Loss |
|---|
| F1 base | 49.55 | 81.17 | 2.2389 |
| F1 + this LoRA | 61.88 | 88.87 | 1.2489 |
| Δ | +12.33 | +7.70 | -0.9900 |
The 10 songs are this genre's slice of a 130-song eval set (10 per genre × 13 genres, seed 42) drawn from the held-out val/test partitions only — pop from McGill Billboard (CC0), jazz from public standards corpora, classical from Bach chorales, the other ten genres from the matching Chordonomicon subsets (CC BY-NC 4.0).
Training data
371 sequences from 237 songs —
Bach chorales curated from the public-domain
music21 corpus, song-level 80/10/10 split (seed 42), 12-key augmentation on train. The chorales themselves are public domain; the CC BY-NC restriction applies because the adapter runs on top of an F1 base trained on Chordonomicon.
Adapter: LoRA on the Q/K/V projections (w_q, w_k, w_v), r=64, α=128, dropout 0.05. The adapter file holds 1,572,864 LoRA parameters (6.0 MB). Training also updates the token-embedding and output matrices (367,616 parameters, shipped in embedding_extension.pt), so the trained set totals 1,940,480 parameters, 7.6% of the 25,665,536 that full fine-tuning updates. Best checkpoint by minimum val loss.
License
CC BY-NC 4.0 (matching Chordonomicon, the upstream training corpus). Research, paper replication, portfolio, and demo use are permitted; commercial use is not.
Citation
1@misc{lee2026chordmix,
2 title = {Empirical Study of Pop and Jazz Mix Ratios for Genre-Adaptive Chord Generation},
3 author = {Lee, Jinju},
4 year = {2026},
5 eprint = {2605.04998},
6 archivePrefix = {arXiv}
7}
8
9@misc{lee2026chordtimeseries,
10 title = {How Far Can Chord-Symbol Time-Series Adaptation Carry Genre Identity?},
11 author = {Lee, Jinju},
12 year = {2026},
13 eprint = {2606.07334},
14 archivePrefix = {arXiv}
15}