Slay micro-models: eksperci (jig/bach/waltz/reel) + kompozycja (E0/fuse/duet)
Browse files- .gitignore +13 -0
- LICENSE +24 -0
- README.md +65 -0
- data/gpt_ckpt.pt +3 -0
- data/loss_log.csv +12 -0
- data/models/bach_ckpt.pt +3 -0
- data/models/reel_ckpt.pt +3 -0
- data/models/reel_sv_ckpt.pt +3 -0
- data/models/waltz_ckpt.pt +3 -0
- reports/raport-baseline.html +169 -0
- requirements.txt +2 -0
- samples/clean_1.abc +10 -0
- samples/clean_1.mid +0 -0
- samples/clean_2.abc +6 -0
- samples/clean_2.mid +0 -0
- samples/clean_3.abc +9 -0
- samples/clean_3.mid +0 -0
- src/abc_to_midi.py +50 -0
- src/analyze.py +40 -0
- src/duet.py +60 -0
- src/e0_stitch.py +79 -0
- src/fuse.py +78 -0
- src/gen_samples.py +60 -0
- src/gpt.py +128 -0
- src/make_midi.py +61 -0
- src/ngram_model.py +62 -0
- src/prepare_bach.py +83 -0
- src/prepare_data.py +69 -0
- src/sample_gpt.py +37 -0
- src/train_gpt.py +115 -0
- src/upload_hf.py +33 -0
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# cache
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
|
| 5 |
+
# Surowe dane i generacje NIE idą do repo (licencja thesession / rozmiar).
|
| 6 |
+
# Idą za to wytrenowane MODELE (wagi) — to nasze artefakty.
|
| 7 |
+
data/*
|
| 8 |
+
!data/gpt_ckpt.pt
|
| 9 |
+
!data/loss_log.csv
|
| 10 |
+
!data/models
|
| 11 |
+
data/models/*
|
| 12 |
+
!data/models/*.pt
|
| 13 |
+
data/models/gpt_ckpt_v1_dirty.pt
|
LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Arkadiusz Słota / Slayer
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
| 23 |
+
Note: training data originates from thesession.org contributors and is not
|
| 24 |
+
redistributed in this repository.
|
README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- music-generation
|
| 5 |
+
- abc-notation
|
| 6 |
+
- symbolic-music
|
| 7 |
+
- gpt
|
| 8 |
+
- char-level
|
| 9 |
+
- from-scratch
|
| 10 |
+
- model-composition
|
| 11 |
+
- stitching
|
| 12 |
+
library_name: pytorch
|
| 13 |
+
pipeline_tag: text-generation
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Slay Micro-Models — tiny from-scratch music experts + composition research
|
| 17 |
+
|
| 18 |
+
A family of **~0.8M-parameter char-level GPTs**, each trained **from scratch** on monophonic music in
|
| 19 |
+
[ABC notation](https://abcnotation.com/), plus experiments in **composing small experts** (stitching,
|
| 20 |
+
ensembling, duets). Built as research for the **Slayer** collective (toward a "small models, composed"
|
| 21 |
+
paper). Music is the sandbox; the methods generalize.
|
| 22 |
+
|
| 23 |
+
Every expert shares one architecture: decoder-only Transformer — 4 layers, 4 heads, `d_model=128`,
|
| 24 |
+
context 128 chars, character-level. Trained on CPU in minutes.
|
| 25 |
+
|
| 26 |
+
## The experts
|
| 27 |
+
| Expert (`data/models/`) | Style / render | Training data | Val perplexity |
|
| 28 |
+
|---|---|---|---|
|
| 29 |
+
| `gpt_ckpt.pt` (jig) | Irish jig, 6/8 | 12.1k tunes (thesession.org) | **3.80** |
|
| 30 |
+
| `bach_ckpt.pt` | Baroque chorale soprano | 350 soprano lines (music21) | 2.09\* |
|
| 31 |
+
| `waltz_ckpt.pt` | Lyrical waltz, 3/4 → piano | 3.0k tunes | ~4.4 |
|
| 32 |
+
| `reel_ckpt.pt` | Driving fiddle, 4/4 → violin | 17.2k tunes | ~4.9 |
|
| 33 |
+
| `reel_sv_ckpt.pt` | reel on shared vocab (for composition) | 17.2k tunes | ~4.9 |
|
| 34 |
+
|
| 35 |
+
\* Bach ppl is **not** directly comparable (smaller vocab + very repetitive data).
|
| 36 |
+
|
| 37 |
+
## Composition experiments
|
| 38 |
+
- **E0 (self-stitch) ✅** — a trained linear mapper at an intermediate seam is lossless (Δppl ≈ 0): the stitching **mechanism** is sound (validates plumbing, not the thesis).
|
| 39 |
+
- **Ensemble fusion** (`src/fuse.py`) — blend two experts' next-token distributions (shared vocab) → audible hybrid. This is the **flat-weighting baseline**.
|
| 40 |
+
- **Duet** (`src/duet.py`) — two experts layered (piano + violin, simultaneous): multi-track, not model-level fusion.
|
| 41 |
+
- **Next — E1:** representation-level stitch (the actual hypothesis, meant to beat these baselines).
|
| 42 |
+
|
| 43 |
+
## Pipeline (`src/`)
|
| 44 |
+
`prepare_data.py` / `prepare_bach.py` (build ABC corpus) → `gpt.py` (architecture) → `train_gpt.py`
|
| 45 |
+
(train; optional shared vocab) → `make_midi.py` / `gen_samples.py` (generate + render) →
|
| 46 |
+
`e0_stitch.py` / `fuse.py` / `duet.py` (composition) · `ngram_model.py` (baseline) · `abc_to_midi.py` (render).
|
| 47 |
+
|
| 48 |
+
## Usage
|
| 49 |
+
```bash
|
| 50 |
+
pip install torch music21
|
| 51 |
+
python src/gen_samples.py --ckpt data/models/waltz_ckpt.pt --meter 3/4 --keys D,G,Emin --inst piano --out out
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Honest scope
|
| 55 |
+
**Shown:** a small char-LM learns real musical structure (meter, key signatures, cadences) from
|
| 56 |
+
next-token prediction *alone*; data cleaning measurably helps (ppl 3.88→3.80); the stitch mechanism is
|
| 57 |
+
lossless; experts can be combined (baseline). **Not yet shown:** that representation-level composition of
|
| 58 |
+
small experts beats a single model — the open hypothesis (E1+).
|
| 59 |
+
|
| 60 |
+
## Data & license
|
| 61 |
+
Code & weights: **MIT**. Training data **not redistributed** — folk tunes from
|
| 62 |
+
[thesession.org](https://thesession.org/) (rebuild via `prepare_data.py`); Bach chorale sopranos via
|
| 63 |
+
`music21`. Please respect source terms.
|
| 64 |
+
|
| 65 |
+
Built by Arkadiusz Słota for the **Slayer** collective. Educational / research project.
|
data/gpt_ckpt.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:886989d488609c2a69cbce40426c490bacb4054b3ade4ffa8efc7c1573d32c0c
|
| 3 |
+
size 3548345
|
data/loss_log.csv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
iter,train_loss,val_loss
|
| 2 |
+
0,3.9971,3.997
|
| 3 |
+
200,2.2354,2.2299
|
| 4 |
+
400,1.8646,1.8573
|
| 5 |
+
600,1.6185,1.6163
|
| 6 |
+
800,1.5349,1.5443
|
| 7 |
+
1000,1.4724,1.4663
|
| 8 |
+
1200,1.4319,1.4304
|
| 9 |
+
1400,1.3977,1.3966
|
| 10 |
+
1600,1.3595,1.3655
|
| 11 |
+
1800,1.3591,1.3523
|
| 12 |
+
2000,1.3314,1.3346
|
data/models/bach_ckpt.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5066f41719d4509f1391c298be02382b6204e83ece0e38a0240c7812847036a3
|
| 3 |
+
size 3539895
|
data/models/reel_ckpt.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6504e8633a1533ba01e0df10827b6ee261efcae24065df97d7c4d9e153b851be
|
| 3 |
+
size 3548407
|
data/models/reel_sv_ckpt.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:149711e857fd356aa00e45065f51890849c37d1e603839fd4c8249bea0d86548
|
| 3 |
+
size 3549105
|
data/models/waltz_ckpt.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d2a982c5cd7e2b71353185d1fe5a39b0731e8a1a0caf8c5ca55071abedb24c5
|
| 3 |
+
size 3548981
|
reports/raport-baseline.html
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<title>Raport — Baseline generatora muzyki ABC (n-gram)</title>
|
| 2 |
+
<style>
|
| 3 |
+
:root{
|
| 4 |
+
--ink:#1a1726; --muted:#6b6580; --line:#e7e3f0; --bg:#faf9fd;
|
| 5 |
+
--accent:#6d4aff; --accent2:#12a594; --warn:#c2410c; --paper:#fff;
|
| 6 |
+
}
|
| 7 |
+
*{box-sizing:border-box}
|
| 8 |
+
.wrap{max-width:880px;margin:0 auto;padding:36px 22px 80px;
|
| 9 |
+
font-family:"Segoe UI",system-ui,-apple-system,sans-serif;color:var(--ink);
|
| 10 |
+
background:var(--bg);line-height:1.62;font-size:16px;-webkit-font-smoothing:antialiased}
|
| 11 |
+
h1{font-size:30px;line-height:1.2;margin:0 0 6px;letter-spacing:-.02em}
|
| 12 |
+
h2{font-size:20px;margin:38px 0 12px;padding-top:18px;border-top:1px solid var(--line);letter-spacing:-.01em}
|
| 13 |
+
h3{font-size:15px;margin:22px 0 6px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em}
|
| 14 |
+
p{margin:10px 0}
|
| 15 |
+
.sub{color:var(--muted);font-size:14.5px;margin:0 0 4px}
|
| 16 |
+
.tag{display:inline-block;background:var(--accent);color:#fff;font-size:11px;
|
| 17 |
+
font-weight:700;letter-spacing:.08em;padding:3px 9px;border-radius:20px;text-transform:uppercase}
|
| 18 |
+
.abstract{background:var(--paper);border:1px solid var(--line);border-left:4px solid var(--accent);
|
| 19 |
+
border-radius:10px;padding:16px 20px;margin:22px 0;font-size:15.5px}
|
| 20 |
+
.cards{display:flex;gap:12px;flex-wrap:wrap;margin:18px 0}
|
| 21 |
+
.card{flex:1;min-width:150px;background:var(--paper);border:1px solid var(--line);
|
| 22 |
+
border-radius:12px;padding:16px 18px}
|
| 23 |
+
.card .n{font-size:26px;font-weight:800;color:var(--accent);letter-spacing:-.02em}
|
| 24 |
+
.card .l{font-size:13px;color:var(--muted);margin-top:2px}
|
| 25 |
+
.chart{background:var(--paper);border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin:14px 0}
|
| 26 |
+
.cap{font-size:13px;color:var(--muted);margin-top:10px}
|
| 27 |
+
/* vertical bars */
|
| 28 |
+
.barv-row{display:flex;align-items:flex-end;gap:10px;height:220px;margin-top:6px}
|
| 29 |
+
.barv-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end}
|
| 30 |
+
.barv{width:100%;border-radius:6px 6px 0 0;background:linear-gradient(180deg,#8a6bff,#6d4aff)}
|
| 31 |
+
.barv-v{font-size:12.5px;font-weight:700;margin-bottom:5px;color:var(--ink)}
|
| 32 |
+
.barv-x{font-size:12px;color:var(--muted);margin-top:7px}
|
| 33 |
+
/* horizontal bars */
|
| 34 |
+
.barh{display:grid;grid-template-columns:54px 1fr;gap:8px;align-items:center;margin:5px 0}
|
| 35 |
+
.barh .k{font-family:ui-monospace,Consolas,monospace;font-size:13px;text-align:right;color:var(--muted)}
|
| 36 |
+
.track{background:#f0edf8;border-radius:6px;height:22px;overflow:hidden;position:relative}
|
| 37 |
+
.fill{height:100%;background:linear-gradient(90deg,#6d4aff,#9a82ff);border-radius:6px;
|
| 38 |
+
display:flex;align-items:center;justify-content:flex-end;padding-right:7px;color:#fff;font-size:11.5px;font-weight:700}
|
| 39 |
+
/* stacked novelty */
|
| 40 |
+
.stack{display:flex;height:30px;border-radius:7px;overflow:hidden;margin:7px 0;font-size:12px;font-weight:700;color:#fff}
|
| 41 |
+
.seg-copy{background:#b8b2cc;display:flex;align-items:center;justify-content:center}
|
| 42 |
+
.seg-new{background:var(--accent2);display:flex;align-items:center;justify-content:center}
|
| 43 |
+
.skey{font-family:ui-monospace,Consolas,monospace;font-size:13px;color:var(--muted);margin-right:8px;min-width:60px;display:inline-block}
|
| 44 |
+
.finding{background:#eafaf6;border:1px solid #b9ece1;border-radius:10px;padding:14px 18px;margin:18px 0}
|
| 45 |
+
.finding b{color:var(--accent2)}
|
| 46 |
+
.warnbox{background:#fff5ed;border:1px solid #fcd9bf;border-radius:10px;padding:14px 18px;margin:18px 0}
|
| 47 |
+
pre{background:#1a1726;color:#e8e3f7;border-radius:10px;padding:16px 18px;overflow-x:auto;
|
| 48 |
+
font-family:ui-monospace,Consolas,monospace;font-size:13px;line-height:1.5}
|
| 49 |
+
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:14.5px}
|
| 50 |
+
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
|
| 51 |
+
th{color:var(--muted);font-weight:600;font-size:13px}
|
| 52 |
+
.ok{color:var(--accent2);font-weight:700}
|
| 53 |
+
.no{color:var(--warn);font-weight:700}
|
| 54 |
+
footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--line);color:var(--muted);font-size:13px}
|
| 55 |
+
.flow{font-family:ui-monospace,Consolas,monospace;font-size:13.5px;background:var(--paper);
|
| 56 |
+
border:1px solid var(--line);border-radius:10px;padding:14px 16px;text-align:center;color:var(--accent)}
|
| 57 |
+
</style>
|
| 58 |
+
|
| 59 |
+
<div class="wrap">
|
| 60 |
+
|
| 61 |
+
<span class="tag">Slayer · NaukaML · Capstone (baseline)</span>
|
| 62 |
+
<h1>Generator melodii fortepianowych metodą n-gram</h1>
|
| 63 |
+
<p class="sub">Raport z eksperymentu · 2026-06-20 · Arkadiusz Słota</p>
|
| 64 |
+
<p class="sub">Korpus: 11 404 jigów 6/8 (thesession.org) · model i kod własne, bez bibliotek ML</p>
|
| 65 |
+
|
| 66 |
+
<div class="abstract">
|
| 67 |
+
<b>Streszczenie.</b> Zbudowano od podstaw statystyczny model językowy (n-gram rzędu 6)
|
| 68 |
+
przewidujący kolejny <i>znak</i> notacji ABC i generujący nowe melodie taneczne (jigi 6/8).
|
| 69 |
+
Model — w całości napisany w czystym Pythonie — odtwarza strukturę metryczną, kreski taktowe
|
| 70 |
+
i kadencje <b>bez żadnej zakodowanej wiedzy muzycznej</b>, wyłącznie ze statystyki 2,3 mln znaków.
|
| 71 |
+
Wygenerowane utwory przekonwertowano do MIDI (fortepian). Praca stanowi <b>punkt odniesienia</b>
|
| 72 |
+
dla planowanego modelu neuronowego (GPT od zera).
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div class="cards">
|
| 76 |
+
<div class="card"><div class="n">2 321 606</div><div class="l">znaków w korpusie</div></div>
|
| 77 |
+
<div class="card"><div class="n">53</div><div class="l">znaki w słowniku</div></div>
|
| 78 |
+
<div class="card"><div class="n">306 858</div><div class="l">unikalne konteksty (rząd 6)</div></div>
|
| 79 |
+
<div class="card"><div class="n">5 / 5</div><div class="l">melodii → MIDI</div></div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<h2>1. Metoda</h2>
|
| 83 |
+
<p>Muzykę potraktowano jako <b>tekst</b> w notacji ABC, a generację jako zadanie „przewidź następny token"
|
| 84 |
+
— identycznie jak w modelach językowych, tylko na poziomie znaku. Pipeline:</p>
|
| 85 |
+
<p class="flow">dane (ABC) → zliczanie n-gramów → próbkowanie z backoffem → ABC → MIDI (fortepian)</p>
|
| 86 |
+
<p>Model przechowuje, dla każdego kontekstu długości 0–6 znaków, rozkład następnego znaku.
|
| 87 |
+
Przy generacji stosuje <b>backoff</b>: używa najdłuższego zaobserwowanego kontekstu, a gdy go brak —
|
| 88 |
+
skraca. Próbkowanie z temperaturą 0,7. Uczenie = zliczanie (brak wag, gradientu, GPU).</p>
|
| 89 |
+
|
| 90 |
+
<h2>2. Struktura danych: „pamięć" modelu</h2>
|
| 91 |
+
<div class="chart">
|
| 92 |
+
<div class="barv-row">
|
| 93 |
+
<div class="barv-col"><div class="barv-v">53</div><div class="barv" style="height:31%"></div><div class="barv-x">1</div></div>
|
| 94 |
+
<div class="barv-col"><div class="barv-v">1 295</div><div class="barv" style="height:57%"></div><div class="barv-x">2</div></div>
|
| 95 |
+
<div class="barv-col"><div class="barv-v">13 410</div><div class="barv" style="height:75%"></div><div class="barv-x">3</div></div>
|
| 96 |
+
<div class="barv-col"><div class="barv-v">56 461</div><div class="barv" style="height:87%"></div><div class="barv-x">4</div></div>
|
| 97 |
+
<div class="barv-col"><div class="barv-v">154 688</div><div class="barv" style="height:94%"></div><div class="barv-x">5</div></div>
|
| 98 |
+
<div class="barv-col"><div class="barv-v">306 858</div><div class="barv" style="height:100%"></div><div class="barv-x">6</div></div>
|
| 99 |
+
</div>
|
| 100 |
+
<div class="cap">Ryc. 1 — Liczba unikalnych kontekstów wg długości n-gramu (skala log). Im dłuższy kontekst,
|
| 101 |
+
tym więcej różnych wzorców — i tym bliżej do zapamiętywania pojedynczych fragmentów.</div>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<h2>3. Z czego zbudowana jest notacja ABC</h2>
|
| 105 |
+
<div class="chart">
|
| 106 |
+
<div class="barh"><div class="k">spacja</div><div class="track"><div class="fill" style="width:100%">291 488</div></div></div>
|
| 107 |
+
<div class="barh"><div class="k">|</div><div class="track"><div class="fill" style="width:92.6%">269 821</div></div></div>
|
| 108 |
+
<div class="barh"><div class="k">A</div><div class="track"><div class="fill" style="width:61.6%">179 528</div></div></div>
|
| 109 |
+
<div class="barh"><div class="k">d</div><div class="track"><div class="fill" style="width:54.7%">159 584</div></div></div>
|
| 110 |
+
<div class="barh"><div class="k">B</div><div class="track"><div class="fill" style="width:53.8%">156 938</div></div></div>
|
| 111 |
+
<div class="barh"><div class="k">e</div><div class="track"><div class="fill" style="width:43%">125 418</div></div></div>
|
| 112 |
+
<div class="barh"><div class="k">G</div><div class="track"><div class="fill" style="width:38.8%">113 138</div></div></div>
|
| 113 |
+
<div class="barh"><div class="k">2</div><div class="track"><div class="fill" style="width:35.2%">102 474</div></div></div>
|
| 114 |
+
<div class="barh"><div class="k">:</div><div class="track"><div class="fill" style="width:26.4%">76 971</div></div></div>
|
| 115 |
+
<div class="cap">Ryc. 2 — Najczęstsze znaki w korpusie. Dominują <b>spacja</b> i <b>kreska taktowa „|"</b>
|
| 116 |
+
(rytm/struktura), potem nuty (A, d, B…) — model najpierw uczy się „rusztowania" utworu.</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<h2>4. Wynik kluczowy: kopiuje czy komponuje?</h2>
|
| 120 |
+
<p>Sprawdzono, jaki procent fragmentów wygenerowanej muzyki <b>występuje dosłownie</b> w danych treningowych
|
| 121 |
+
(im dłuższe okno, tym ostrzejszy test oryginalności):</p>
|
| 122 |
+
<div class="chart">
|
| 123 |
+
<div><span class="skey">okno 4</span></div>
|
| 124 |
+
<div class="stack"><div class="seg-copy" style="width:97.7%">97,7% skopiowane</div><div class="seg-new" style="width:2.3%">2,3%</div></div>
|
| 125 |
+
<div><span class="skey">okno 6</span></div>
|
| 126 |
+
<div class="stack"><div class="seg-copy" style="width:94.2%">94,2% skopiowane</div><div class="seg-new" style="width:5.8%">5,8%</div></div>
|
| 127 |
+
<div><span class="skey">okno 8</span></div>
|
| 128 |
+
<div class="stack"><div class="seg-copy" style="width:83.9%">83,9% skopiowane</div><div class="seg-new" style="width:16.1%">16,1% nowe</div></div>
|
| 129 |
+
<div class="cap">Ryc. 3 — Udział fragmentów skopiowanych (szare) vs nowych (zielone) wg długości okna.</div>
|
| 130 |
+
</div>
|
| 131 |
+
<div class="finding">
|
| 132 |
+
<b>Wniosek:</b> na krótkich oknach model niemal wyłącznie powtarza zapamiętane wzorce (97,7%),
|
| 133 |
+
ale na dłuższych <b>16,1% sekwencji jest nowych</b> — model <b>skleja zapamiętane fragmenty
|
| 134 |
+
w nowe, niewidziane wcześniej całości</b>. To jest „kompozycja" n-gramu: lokalnie poprawna,
|
| 135 |
+
globalnie świeża.
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<h2>5. Dyskusja — uczciwie o ograniczeniach</h2>
|
| 139 |
+
<div class="warnbox">
|
| 140 |
+
To <b>model statystyczny (n-gram), nie sieć neuronowa.</b> „Widzi" maksymalnie 6 znaków wstecz,
|
| 141 |
+
więc nie ma <b>długiego łuku formalnego</b> (powtórzenie tematu, budowa AABB), a MIDI jest
|
| 142 |
+
mechaniczne (stała głośność, brak pedału i frazowania). Wysoki odsetek kopiowania to wprost
|
| 143 |
+
skutek braku uogólniania — czego nauczyliśmy się jako granicy małych/prostych modeli (L04, L08).
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<h2>6. Następny krok (Tor 2)</h2>
|
| 147 |
+
<table>
|
| 148 |
+
<tr><th>Aspekt</th><th>Teraz (baseline)</th><th>GPT neuronowy</th></tr>
|
| 149 |
+
<tr><td>Mechanizm</td><td>zliczanie n-gramów</td><td>wagi + gradient (backprop)</td></tr>
|
| 150 |
+
<tr><td>Pamięć kontekstu</td><td class="no">6 znaków</td><td class="ok">setki tokenów (uwaga)</td></tr>
|
| 151 |
+
<tr><td>Uogólnianie</td><td class="no">słabe (kopiuje)</td><td class="ok">realne</td></tr>
|
| 152 |
+
<tr><td>Sprzęt</td><td>CPU, sekundy</td><td>GPU (RTX 3050)</td></tr>
|
| 153 |
+
<tr><td>Cel docelowy</td><td>jigi ABC</td><td class="ok">fortepian, styl Chopina</td></tr>
|
| 154 |
+
</table>
|
| 155 |
+
<p>Baseline będzie <b>miarą</b>, o ile model neuronowy poprawi spójność i oryginalność.</p>
|
| 156 |
+
|
| 157 |
+
<h2>Aneks — przykładowa wygenerowana melodia</h2>
|
| 158 |
+
<pre>X:1
|
| 159 |
+
M:6/8
|
| 160 |
+
K:G
|
| 161 |
+
|:DED F2A|d2f a2f|fge fdB|AdB AFD|FED E2 A|cA/B/c dcd|
|
| 162 |
+
efe edB|dBA GBd|~g3 gfg|afd e2 g|f2 d def|efe edB|AGF Gdc:|</pre>
|
| 163 |
+
|
| 164 |
+
<footer>
|
| 165 |
+
Eksperyment wykonany w ramach kursu NaukaML (Slayer). Model i kod: własne (czysty Python).
|
| 166 |
+
Konwersja ABC→MIDI: music21. Dane: thesession.org. Metryki liczone na 2,3 mln znaków korpusu.
|
| 167 |
+
</footer>
|
| 168 |
+
|
| 169 |
+
</div>
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.0
|
| 2 |
+
music21>=9.0
|
samples/clean_1.abc
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
X:1
|
| 2 |
+
M:6/8
|
| 3 |
+
K:G
|
| 4 |
+
|:Edc B2A|EDE FED|EGA AFD|EDE DED|
|
| 5 |
+
EGA FEE|DED EFE|GBA AFE|1 AGE G2 :|2 AGE D2G||
|
| 6 |
+
|:DEG Ade|gfg efe|fed cBA|G2A GED|
|
| 7 |
+
DDE AEB|BGA ded|efe dBA|AAG AFE|
|
| 8 |
+
AFA Adc|dcd BAF|Adc BGD|
|
| 9 |
+
DFA ded|ded gfd|Bcd gfe|faf fdB|
|
| 10 |
+
cdf edB|ddd f2e|cdA AFD|DFD D2:|
|
samples/clean_1.mid
ADDED
|
Binary file (1.5 kB). View file
|
|
|
samples/clean_2.abc
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
X:1
|
| 2 |
+
M:6/8
|
| 3 |
+
K:G
|
| 4 |
+
|:D|GAF EFG|Bde dAB|ABG FDB|1 DAB B2d:|2 edB d2B:||2 gfg fdB|
|
| 5 |
+
GB ABd|GBd g2e|dBd gfe|abd egf|
|
| 6 |
+
gag gfd|cBG GAG|BAG GAG|AGE D2:|
|
samples/clean_2.mid
ADDED
|
Binary file (774 Bytes). View file
|
|
|
samples/clean_3.abc
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
X:1
|
| 2 |
+
M:6/8
|
| 3 |
+
K:G
|
| 4 |
+
|:dBA cAF|GBG BAF|FDF AFD|dcd cAF|
|
| 5 |
+
d2A AFC|AFA cAF|BAd cBF|DFD DFF|
|
| 6 |
+
GFA AFD|DBG cBc|dcB cBA|cdc d2:|
|
| 7 |
+
|:DDED AFD|DFG AFF|FDD FED|dDG D2F|
|
| 8 |
+
DFD EFD|dfd d2d|ddB FGE|DED D2C F|
|
| 9 |
+
DDFD FEA|cdd cAG|(BAF BAF|1 FDD D2E:|2 FFG G2 E||
|
samples/clean_3.mid
ADDED
|
Binary file (1.37 kB). View file
|
|
|
src/abc_to_midi.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Konwersja wygenerowanych melodii ABC -> MIDI (fortepian) przez music21.
|
| 2 |
+
Każda melodia osobno; błędy parsowania nie wywalają całości.
|
| 3 |
+
"""
|
| 4 |
+
import sys, re
|
| 5 |
+
from music21 import converter, instrument
|
| 6 |
+
|
| 7 |
+
def sanitize(abc: str) -> str:
|
| 8 |
+
"""Usuwa powtórzenia/volty z ciała, ALE ZOSTAWIA akordy [ace]. Nagłówki nietknięte."""
|
| 9 |
+
out = []
|
| 10 |
+
for ln in abc.split("\n"):
|
| 11 |
+
if re.match(r"^[A-Za-z]:", ln): # nagłówek
|
| 12 |
+
out.append(ln); continue
|
| 13 |
+
b = re.sub(r"\[[12]", "", ln) # volty [1 [2 (akordy [ace] ZOSTAJĄ!)
|
| 14 |
+
b = b.replace(":", "") # dwukropki powtórzeń (|: :| ::)
|
| 15 |
+
b = re.sub(r"\|\s*[12]", "|", b) # numery volt po kresce |1 |2
|
| 16 |
+
b = re.sub(r"\|+", "|", b) # scal wielokrotne kreski
|
| 17 |
+
b = re.sub(r"\^=|=\^|_=|=_", lambda m: m.group(0).replace("=", ""), b) # zepsute akcydencje (^=)
|
| 18 |
+
out.append(b)
|
| 19 |
+
return "\n".join(out)
|
| 20 |
+
|
| 21 |
+
def to_midi(abc: str, out_path: str, inst=None) -> bool:
|
| 22 |
+
try:
|
| 23 |
+
score = converter.parse(sanitize(abc), format="abc")
|
| 24 |
+
# spłaszcz do samych nut/pauz (omija konflikt TimeSignature przy budowie taktów)
|
| 25 |
+
notes = score.flatten().notesAndRests.stream()
|
| 26 |
+
if inst is not None: # ustaw instrument (np. Violin); domyślnie fortepian (GM 0)
|
| 27 |
+
notes.insert(0, inst)
|
| 28 |
+
notes.write("midi", fp=out_path)
|
| 29 |
+
return True
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f" [pominięto] {out_path}: {type(e).__name__}: {e}")
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 36 |
+
infile = sys.argv[1] if len(sys.argv) > 1 else "data/generated.abc"
|
| 37 |
+
prefix = sys.argv[2] if len(sys.argv) > 2 else "data/jig"
|
| 38 |
+
text = open(infile, encoding="utf-8").read()
|
| 39 |
+
tunes = [t.strip() for t in text.split("\n\n") if "X:" in t]
|
| 40 |
+
print(f"melodii do konwersji: {len(tunes)} (z {infile})")
|
| 41 |
+
ok = 0
|
| 42 |
+
for i, tune in enumerate(tunes, 1):
|
| 43 |
+
path = f"{prefix}_{i}.mid"
|
| 44 |
+
if to_midi(tune, path):
|
| 45 |
+
print(f" OK -> {path}")
|
| 46 |
+
ok += 1
|
| 47 |
+
print(f"\ngotowe: {ok}/{len(tunes)} plików MIDI (fortepian) w data/")
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
src/analyze.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Metryki do raportu: rozkład znaków, nowość (kopiuje vs komponuje), długości."""
|
| 2 |
+
import sys, collections, json
|
| 3 |
+
|
| 4 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 5 |
+
train = open("data/jigs.abc", encoding="utf-8").read()
|
| 6 |
+
gen = open("data/generated.abc", encoding="utf-8").read()
|
| 7 |
+
|
| 8 |
+
# ciało generacji bez nagłówków (do metryki nowości)
|
| 9 |
+
def bodies(text):
|
| 10 |
+
return "".join(ln for ln in text.split("\n")
|
| 11 |
+
if ln and not ln[:2] in ("X:", "M:", "K:"))
|
| 12 |
+
|
| 13 |
+
gen_body = bodies(gen)
|
| 14 |
+
|
| 15 |
+
# 1) rozkład znaków (top 15) w treningu
|
| 16 |
+
freq = collections.Counter(c for c in train if c != "\n")
|
| 17 |
+
top = freq.most_common(15)
|
| 18 |
+
|
| 19 |
+
# 2) nowość: jaki % k-gramów generacji WYSTĘPUJE w treningu (kopiowanie)
|
| 20 |
+
novelty = {}
|
| 21 |
+
for k in (4, 6, 8):
|
| 22 |
+
tr = set(train[i:i+k] for i in range(len(train)-k))
|
| 23 |
+
gk = [gen_body[i:i+k] for i in range(len(gen_body)-k)]
|
| 24 |
+
seen = sum(1 for g in gk if g in tr)
|
| 25 |
+
novelty[k] = {"skopiowane_%": round(100*seen/len(gk), 1),
|
| 26 |
+
"nowe_%": round(100*(1-seen/len(gk)), 1)}
|
| 27 |
+
del tr
|
| 28 |
+
|
| 29 |
+
# 3) długości wygenerowanych melodii
|
| 30 |
+
tunes = [t for t in gen.split("\n\n") if "X:" in t]
|
| 31 |
+
lens = [len(t) for t in tunes]
|
| 32 |
+
|
| 33 |
+
print(json.dumps({
|
| 34 |
+
"korpus_znaki": len(train),
|
| 35 |
+
"slownik": len(set(train)),
|
| 36 |
+
"top_znaki": top,
|
| 37 |
+
"nowosc": novelty,
|
| 38 |
+
"wygenerowane_melodie": len(tunes),
|
| 39 |
+
"srednia_dlugosc_znaki": round(sum(lens)/len(lens), 1),
|
| 40 |
+
}, ensure_ascii=False, indent=2))
|
src/duet.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DUET: walc (fortepian) + reel (skrzypce) grają RAZEM — dwie ścieżki naraz, ta sama tonacja.
|
| 2 |
+
To NIE fuzja w jedną linię (jak ensemble), tylko dwie niezależne linie złożone jednocześnie.
|
| 3 |
+
"""
|
| 4 |
+
import sys, os
|
| 5 |
+
sys.path.insert(0, "src")
|
| 6 |
+
import torch
|
| 7 |
+
from gpt import GPT
|
| 8 |
+
from abc_to_midi import sanitize
|
| 9 |
+
from music21 import converter, stream, instrument as M, tempo
|
| 10 |
+
|
| 11 |
+
def load(p):
|
| 12 |
+
ck = torch.load(p, map_location="cpu", weights_only=False)
|
| 13 |
+
m = GPT(ck["config"]); m.load_state_dict(ck["model"]); m.eval()
|
| 14 |
+
return m, ck
|
| 15 |
+
|
| 16 |
+
def first_tune(raw):
|
| 17 |
+
out = []
|
| 18 |
+
for ln in raw.split("\n"):
|
| 19 |
+
if ln.startswith("X:") and out:
|
| 20 |
+
break
|
| 21 |
+
out.append(ln)
|
| 22 |
+
return "\n".join(out).strip()
|
| 23 |
+
|
| 24 |
+
@torch.no_grad()
|
| 25 |
+
def gen(model, stoi, itos, seed, n=360, temp=0.85, topk=18):
|
| 26 |
+
idx = torch.tensor([[stoi[c] for c in seed]])
|
| 27 |
+
out = model.generate(idx, n, temperature=temp, top_k=topk)[0].tolist()
|
| 28 |
+
return first_tune("".join(itos[t] for t in out))
|
| 29 |
+
|
| 30 |
+
def to_part(abc, inst):
|
| 31 |
+
sc = converter.parse(sanitize(abc), format="abc")
|
| 32 |
+
notes = sc.flatten().notesAndRests.stream()
|
| 33 |
+
notes.insert(0, inst)
|
| 34 |
+
return notes
|
| 35 |
+
|
| 36 |
+
def main():
|
| 37 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 38 |
+
waltz, ckW = load("data/models/waltz_ckpt.pt")
|
| 39 |
+
reel, ckR = load("data/models/reel_sv_ckpt.pt")
|
| 40 |
+
stoi, itos = ckW["stoi"], ckW["itos"]
|
| 41 |
+
os.makedirs("data/recordings/duet", exist_ok=True)
|
| 42 |
+
torch.manual_seed(20260621)
|
| 43 |
+
ok = 0
|
| 44 |
+
for key in ["D", "G", "Emin"]:
|
| 45 |
+
wabc = gen(waltz, stoi, itos, f"X:1\nM:3/4\nK:{key}\n") # fortepian, walc 3/4
|
| 46 |
+
rabc = gen(reel, stoi, itos, f"X:1\nM:4/4\nK:{key}\n") # skrzypce, reel 4/4
|
| 47 |
+
try:
|
| 48 |
+
sc = stream.Score()
|
| 49 |
+
sc.insert(0, tempo.MetronomeMark(number=96))
|
| 50 |
+
sc.insert(0, to_part(wabc, M.Piano()))
|
| 51 |
+
sc.insert(0, to_part(rabc, M.Violin()))
|
| 52 |
+
path = f"data/recordings/duet/duet_{key}.mid"
|
| 53 |
+
sc.write("midi", fp=path)
|
| 54 |
+
print(f" duet {key} [OK] -> {path}"); ok += 1
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f" duet {key} [błąd]: {type(e).__name__}: {e}")
|
| 57 |
+
print(f"\ngotowe: {ok}/3 duetów w data/recordings/duet/ — fortepian + skrzypce RAZEM 🎻🎹")
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|
src/e0_stitch.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""E0 — self-stitch (benchmark zerowy).
|
| 2 |
+
Wstaw liniowy mapper g (n_embd x n_embd) na styku PO bloku 2 TEGO SAMEGO modelu,
|
| 3 |
+
zamroź resztę. Waliduje MECHANIZM mappera (hydraulikę), NIE tezę.
|
| 4 |
+
- sanity: g = identyczność -> dokładnie baseline.
|
| 5 |
+
- E0: g losowy -> trenuj tylko g -> powinien wrócić do baseline (Δppl≈0).
|
| 6 |
+
Użycie: python src/e0_stitch.py [ckpt] [dane]
|
| 7 |
+
"""
|
| 8 |
+
import sys, math
|
| 9 |
+
sys.path.insert(0, "src")
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
from torch.nn import functional as F
|
| 13 |
+
from gpt import GPT
|
| 14 |
+
|
| 15 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 16 |
+
CKPT = sys.argv[1] if len(sys.argv) > 1 else "data/gpt_ckpt.pt"
|
| 17 |
+
DATA = sys.argv[2] if len(sys.argv) > 2 else "data/jigs.abc"
|
| 18 |
+
SEAM = 2 # g przed blokiem o indeksie 2 = styk po 2 blokach
|
| 19 |
+
TRAIN_STEPS = 500
|
| 20 |
+
|
| 21 |
+
ck = torch.load(CKPT, map_location="cpu", weights_only=False)
|
| 22 |
+
stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
|
| 23 |
+
model = GPT(cfg); model.load_state_dict(ck["model"]); model.eval()
|
| 24 |
+
for p in model.parameters():
|
| 25 |
+
p.requires_grad_(False)
|
| 26 |
+
print(f"model: {model.num_params():,} param | val loss z ckpt: {ck['val_loss']:.4f} | bloków: {cfg.n_layer} | styk po: {SEAM}")
|
| 27 |
+
|
| 28 |
+
text = open(DATA, encoding="utf-8").read()
|
| 29 |
+
data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
|
| 30 |
+
n = int(0.9 * len(data)); train, val = data[:n], data[n:]
|
| 31 |
+
block, bs = cfg.block_size, 32
|
| 32 |
+
|
| 33 |
+
def get_batch(split):
|
| 34 |
+
d = train if split == "train" else val
|
| 35 |
+
ix = torch.randint(len(d) - block, (bs,))
|
| 36 |
+
x = torch.stack([d[i:i+block] for i in ix])
|
| 37 |
+
y = torch.stack([d[i+1:i+1+block] for i in ix])
|
| 38 |
+
return x, y
|
| 39 |
+
|
| 40 |
+
g = nn.Linear(cfg.n_embd, cfg.n_embd)
|
| 41 |
+
|
| 42 |
+
def fwd(idx, targets, use_g):
|
| 43 |
+
B, T = idx.shape
|
| 44 |
+
pos = torch.arange(T)
|
| 45 |
+
x = model.drop(model.tok_emb(idx) + model.pos_emb(pos))
|
| 46 |
+
for i, blk in enumerate(model.blocks):
|
| 47 |
+
if use_g and i == SEAM:
|
| 48 |
+
x = g(x)
|
| 49 |
+
x = blk(x)
|
| 50 |
+
logits = model.head(model.ln_f(x))
|
| 51 |
+
return F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
|
| 52 |
+
|
| 53 |
+
@torch.no_grad()
|
| 54 |
+
def eval_loss(use_g, iters=60, seed=1234):
|
| 55 |
+
torch.manual_seed(seed) # te same batche val dla każdej oceny
|
| 56 |
+
return sum(fwd(*get_batch("val"), use_g).item() for _ in range(iters)) / iters
|
| 57 |
+
|
| 58 |
+
base = eval_loss(use_g=False)
|
| 59 |
+
with torch.no_grad(): # sanity: identyczność
|
| 60 |
+
g.weight.copy_(torch.eye(cfg.n_embd)); g.bias.zero_()
|
| 61 |
+
ident = eval_loss(use_g=True)
|
| 62 |
+
print(f"baseline (bez g): val {base:.4f} | ppl {math.exp(base):.4f}")
|
| 63 |
+
print(f"g = identyczność: val {ident:.4f} | ppl {math.exp(ident):.4f} (sanity: == baseline)")
|
| 64 |
+
|
| 65 |
+
torch.manual_seed(1) # E0: g losowy -> trening tylko g
|
| 66 |
+
nn.init.normal_(g.weight, std=0.02); nn.init.zeros_(g.bias)
|
| 67 |
+
rand0 = eval_loss(use_g=True)
|
| 68 |
+
opt = torch.optim.AdamW(g.parameters(), lr=1e-3)
|
| 69 |
+
for _ in range(TRAIN_STEPS):
|
| 70 |
+
loss = fwd(*get_batch("train"), True)
|
| 71 |
+
opt.zero_grad(); loss.backward(); opt.step()
|
| 72 |
+
trained = eval_loss(use_g=True)
|
| 73 |
+
print(f"g losowy (przed): val {rand0:.4f} | ppl {math.exp(rand0):.4f}")
|
| 74 |
+
print(f"g po treningu ({TRAIN_STEPS}): val {trained:.4f} | ppl {math.exp(trained):.4f}")
|
| 75 |
+
|
| 76 |
+
dppl = math.exp(trained) - math.exp(base)
|
| 77 |
+
print(f"\nΔppl (po treningu - baseline): {dppl:+.4f}")
|
| 78 |
+
print("WERDYKT E0:", "PASS ✅ — mapper bezstratny, mechanizm szwu działa (benchmark zerowy)"
|
| 79 |
+
if abs(dppl) < 0.05 else "FAIL ❌ — bug w mechanizmie szwu, stop")
|
src/fuse.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Złączenie przez ensemble (logit-mix) DWÓCH modeli o WSPÓLNYM słowniku.
|
| 2 |
+
W każdym kroku: logits = alpha*A + (1-alpha)*B -> sampluj. Melodia wychodzi stylistycznie pomiędzy.
|
| 3 |
+
To jest baseline „płaskiego ważenia" (KMS5). Stitch reprezentacji = osobny eksperyment (E1).
|
| 4 |
+
Użycie:
|
| 5 |
+
python src/fuse.py --a data/models/waltz_ckpt.pt --b data/models/reel_sv_ckpt.pt \
|
| 6 |
+
--alpha 0.5 --meter 3/4 --keys D,G,Emin --inst piano --out data/recordings/fuzja
|
| 7 |
+
"""
|
| 8 |
+
import argparse, sys, os, math
|
| 9 |
+
sys.path.insert(0, "src")
|
| 10 |
+
import torch
|
| 11 |
+
from torch.nn import functional as F
|
| 12 |
+
from gpt import GPT
|
| 13 |
+
from abc_to_midi import to_midi
|
| 14 |
+
from music21 import instrument as M
|
| 15 |
+
|
| 16 |
+
INST = {"piano": M.Piano, "violin": M.Violin, "none": None}
|
| 17 |
+
|
| 18 |
+
def load(path):
|
| 19 |
+
ck = torch.load(path, map_location="cpu", weights_only=False)
|
| 20 |
+
m = GPT(ck["config"]); m.load_state_dict(ck["model"]); m.eval()
|
| 21 |
+
return m, ck
|
| 22 |
+
|
| 23 |
+
def first_tune(raw):
|
| 24 |
+
out = []
|
| 25 |
+
for ln in raw.split("\n"):
|
| 26 |
+
if ln.startswith("X:") and out:
|
| 27 |
+
break
|
| 28 |
+
out.append(ln)
|
| 29 |
+
return "\n".join(out).strip()
|
| 30 |
+
|
| 31 |
+
@torch.no_grad()
|
| 32 |
+
def gen_mix(A, B, idx, n, alpha, temp, topk, block):
|
| 33 |
+
for _ in range(n):
|
| 34 |
+
ic = idx[:, -block:]
|
| 35 |
+
la, _ = A(ic); lb, _ = B(ic)
|
| 36 |
+
logits = (alpha * la[:, -1, :] + (1 - alpha) * lb[:, -1, :]) / temp
|
| 37 |
+
if topk:
|
| 38 |
+
v, _ = torch.topk(logits, topk)
|
| 39 |
+
logits[logits < v[:, [-1]]] = float("-inf")
|
| 40 |
+
probs = F.softmax(logits, dim=-1)
|
| 41 |
+
idx = torch.cat([idx, torch.multinomial(probs, 1)], dim=1)
|
| 42 |
+
return idx
|
| 43 |
+
|
| 44 |
+
def main():
|
| 45 |
+
ap = argparse.ArgumentParser()
|
| 46 |
+
ap.add_argument("--a", required=True); ap.add_argument("--b", required=True)
|
| 47 |
+
ap.add_argument("--alpha", type=float, default=0.5)
|
| 48 |
+
ap.add_argument("--meter", default="3/4"); ap.add_argument("--keys", default="D,G,Emin")
|
| 49 |
+
ap.add_argument("--inst", default="piano", choices=list(INST))
|
| 50 |
+
ap.add_argument("--out", required=True)
|
| 51 |
+
ap.add_argument("--new", type=int, default=420); ap.add_argument("--temp", type=float, default=0.85)
|
| 52 |
+
ap.add_argument("--topk", type=int, default=18)
|
| 53 |
+
a = ap.parse_args()
|
| 54 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 55 |
+
|
| 56 |
+
A, ckA = load(a.a); B, ckB = load(a.b)
|
| 57 |
+
assert ckA["stoi"] == ckB["stoi"], "Modele mają RÓŻNY słownik — najpierw wspólny słownik!"
|
| 58 |
+
stoi, itos = ckA["stoi"], ckA["itos"]
|
| 59 |
+
block = ckA["config"].block_size
|
| 60 |
+
os.makedirs(a.out, exist_ok=True)
|
| 61 |
+
inst_cls = INST[a.inst]
|
| 62 |
+
print(f"FUZJA α={a.alpha} (A={a.a} : B={a.b}) | wspólny słownik {len(stoi)} | -> {a.out}")
|
| 63 |
+
torch.manual_seed(20260621)
|
| 64 |
+
ok = 0
|
| 65 |
+
for i, key in enumerate(a.keys.split(","), 1):
|
| 66 |
+
seed = f"X:1\nM:{a.meter}\nK:{key}\n"
|
| 67 |
+
idx = torch.tensor([[stoi[c] for c in seed]])
|
| 68 |
+
gen = gen_mix(A, B, idx, a.new, a.alpha, a.temp, a.topk, block)[0].tolist()
|
| 69 |
+
tune = first_tune("".join(itos[t] for t in gen))
|
| 70 |
+
base = f"{a.out}/fuzja_{i}_{key}"
|
| 71 |
+
open(base + ".abc", "w", encoding="utf-8").write(tune + "\n")
|
| 72 |
+
good = to_midi(tune, base + ".mid", inst=inst_cls() if inst_cls else None)
|
| 73 |
+
ok += good
|
| 74 |
+
print(f" #{i} ({key}) [{'MIDI OK' if good else 'błąd'}] -> {base}.mid")
|
| 75 |
+
print(f"\ngotowe: {ok}/{len(a.keys.split(','))} fuzji w {a.out}/")
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
main()
|
src/gen_samples.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generuje N próbek z wytrenowanego modelu i renderuje do MIDI z wybranym instrumentem.
|
| 2 |
+
Po jednej melodii na podaną tonację. Zapisuje ABC + MIDI do osobnego katalogu.
|
| 3 |
+
Użycie:
|
| 4 |
+
python src/gen_samples.py --ckpt data/models/waltz_ckpt.pt --meter 3/4 --keys D,G,C,Emin,Amin --inst piano --out data/recordings/waltz
|
| 5 |
+
python src/gen_samples.py --ckpt data/models/reel_ckpt.pt --meter 4/4 --keys D,G,A,Emin,Bmin --inst violin --out data/recordings/reel
|
| 6 |
+
"""
|
| 7 |
+
import argparse, sys, os
|
| 8 |
+
sys.path.insert(0, "src")
|
| 9 |
+
import torch
|
| 10 |
+
from gpt import GPT
|
| 11 |
+
from abc_to_midi import to_midi
|
| 12 |
+
from music21 import instrument as M
|
| 13 |
+
|
| 14 |
+
INST = {"piano": M.Piano, "violin": M.Violin, "guitar": M.AcousticGuitar, "none": None}
|
| 15 |
+
|
| 16 |
+
def first_tune(raw):
|
| 17 |
+
lines = []
|
| 18 |
+
for ln in raw.split("\n"):
|
| 19 |
+
if ln.startswith("X:") and lines:
|
| 20 |
+
break
|
| 21 |
+
lines.append(ln)
|
| 22 |
+
return "\n".join(lines).strip()
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
ap = argparse.ArgumentParser()
|
| 26 |
+
ap.add_argument("--ckpt", required=True)
|
| 27 |
+
ap.add_argument("--meter", default="4/4")
|
| 28 |
+
ap.add_argument("--keys", default="D,G,A,Emin,Bmin")
|
| 29 |
+
ap.add_argument("--inst", default="none", choices=list(INST))
|
| 30 |
+
ap.add_argument("--out", required=True)
|
| 31 |
+
ap.add_argument("--new", type=int, default=420)
|
| 32 |
+
ap.add_argument("--temp", type=float, default=0.85)
|
| 33 |
+
ap.add_argument("--topk", type=int, default=18)
|
| 34 |
+
a = ap.parse_args()
|
| 35 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 36 |
+
|
| 37 |
+
ck = torch.load(a.ckpt, map_location="cpu", weights_only=False)
|
| 38 |
+
stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
|
| 39 |
+
model = GPT(cfg); model.load_state_dict(ck["model"]); model.eval()
|
| 40 |
+
os.makedirs(a.out, exist_ok=True)
|
| 41 |
+
inst_cls = INST[a.inst]
|
| 42 |
+
print(f"{a.ckpt} | val {ck['val_loss']:.3f} | instrument: {a.inst} -> {a.out}")
|
| 43 |
+
torch.manual_seed(20260621)
|
| 44 |
+
ok = 0
|
| 45 |
+
for i, key in enumerate(a.keys.split(","), 1):
|
| 46 |
+
seed = f"X:1\nM:{a.meter}\nK:{key}\n"
|
| 47 |
+
if any(c not in stoi for c in seed):
|
| 48 |
+
print(f" #{i} ({key}): seed ma znak spoza słownika — pomijam"); continue
|
| 49 |
+
idx = torch.tensor([[stoi[c] for c in seed]])
|
| 50 |
+
gen = model.generate(idx, a.new, temperature=a.temp, top_k=a.topk)[0].tolist()
|
| 51 |
+
tune = first_tune("".join(itos[t] for t in gen))
|
| 52 |
+
base = f"{a.out}/sample_{i}_{key}"
|
| 53 |
+
open(base + ".abc", "w", encoding="utf-8").write(tune + "\n")
|
| 54 |
+
good = to_midi(tune, base + ".mid", inst=inst_cls() if inst_cls else None)
|
| 55 |
+
ok += good
|
| 56 |
+
print(f" #{i} ({key}) [{'MIDI OK' if good else 'błąd'}] -> {base}.mid")
|
| 57 |
+
print(f"\ngotowe: {ok}/{len(a.keys.split(','))} próbek w {a.out}/")
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|
src/gpt.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mały GPT od zera (char-level) — architektura z lekcji L07.
|
| 2 |
+
Embedding -> [Blok: uwaga (Q/K/V + maska) + MLP/GELU + residual + LayerNorm] x N -> logity.
|
| 3 |
+
Czysty PyTorch, bez gotowych warstw transformera.
|
| 4 |
+
"""
|
| 5 |
+
import math
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
from torch.nn import functional as F
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class GPTConfig:
|
| 14 |
+
vocab_size: int = 53
|
| 15 |
+
block_size: int = 256 # ile znaków kontekstu (uwaga sięga tak daleko)
|
| 16 |
+
n_layer: int = 4
|
| 17 |
+
n_head: int = 4
|
| 18 |
+
n_embd: int = 128
|
| 19 |
+
dropout: float = 0.1
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class CausalSelfAttention(nn.Module):
|
| 23 |
+
"""Uwaga z maską przyczynową (L07): token patrzy tylko wstecz."""
|
| 24 |
+
def __init__(self, cfg: GPTConfig):
|
| 25 |
+
super().__init__()
|
| 26 |
+
assert cfg.n_embd % cfg.n_head == 0
|
| 27 |
+
self.qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd) # Q, K, V naraz
|
| 28 |
+
self.proj = nn.Linear(cfg.n_embd, cfg.n_embd)
|
| 29 |
+
self.attn_drop = nn.Dropout(cfg.dropout)
|
| 30 |
+
self.resid_drop = nn.Dropout(cfg.dropout)
|
| 31 |
+
self.n_head = cfg.n_head
|
| 32 |
+
self.n_embd = cfg.n_embd
|
| 33 |
+
# maska dolnotrójkątna: pozycja t widzi tylko <= t
|
| 34 |
+
self.register_buffer("mask", torch.tril(torch.ones(cfg.block_size, cfg.block_size))
|
| 35 |
+
.view(1, 1, cfg.block_size, cfg.block_size))
|
| 36 |
+
|
| 37 |
+
def forward(self, x):
|
| 38 |
+
B, T, C = x.shape
|
| 39 |
+
q, k, v = self.qkv(x).split(self.n_embd, dim=2)
|
| 40 |
+
# rozbij na głowice: (B, nh, T, hs)
|
| 41 |
+
hs = C // self.n_head
|
| 42 |
+
q = q.view(B, T, self.n_head, hs).transpose(1, 2)
|
| 43 |
+
k = k.view(B, T, self.n_head, hs).transpose(1, 2)
|
| 44 |
+
v = v.view(B, T, self.n_head, hs).transpose(1, 2)
|
| 45 |
+
# scores = Q·Kᵀ / √d (L07: /√d normalizuje wariancję)
|
| 46 |
+
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(hs))
|
| 47 |
+
att = att.masked_fill(self.mask[:, :, :T, :T] == 0, float("-inf"))
|
| 48 |
+
att = self.attn_drop(F.softmax(att, dim=-1))
|
| 49 |
+
y = att @ v # ważona suma V
|
| 50 |
+
y = y.transpose(1, 2).contiguous().view(B, T, C)
|
| 51 |
+
return self.resid_drop(self.proj(y))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class MLP(nn.Module):
|
| 55 |
+
"""Przetwarzanie w tokenie (L07): 768->4x->768 z GELU."""
|
| 56 |
+
def __init__(self, cfg: GPTConfig):
|
| 57 |
+
super().__init__()
|
| 58 |
+
self.fc = nn.Linear(cfg.n_embd, 4 * cfg.n_embd)
|
| 59 |
+
self.proj = nn.Linear(4 * cfg.n_embd, cfg.n_embd)
|
| 60 |
+
self.drop = nn.Dropout(cfg.dropout)
|
| 61 |
+
|
| 62 |
+
def forward(self, x):
|
| 63 |
+
return self.drop(self.proj(F.gelu(self.fc(x))))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class Block(nn.Module):
|
| 67 |
+
"""Residual + LayerNorm wokół uwagi i MLP (L07: autostrada x + f(x))."""
|
| 68 |
+
def __init__(self, cfg: GPTConfig):
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.ln1 = nn.LayerNorm(cfg.n_embd)
|
| 71 |
+
self.attn = CausalSelfAttention(cfg)
|
| 72 |
+
self.ln2 = nn.LayerNorm(cfg.n_embd)
|
| 73 |
+
self.mlp = MLP(cfg)
|
| 74 |
+
|
| 75 |
+
def forward(self, x):
|
| 76 |
+
x = x + self.attn(self.ln1(x)) # komunikacja MIĘDZY tokenami
|
| 77 |
+
x = x + self.mlp(self.ln2(x)) # przetwarzanie W tokenie
|
| 78 |
+
return x
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class GPT(nn.Module):
|
| 82 |
+
def __init__(self, cfg: GPTConfig):
|
| 83 |
+
super().__init__()
|
| 84 |
+
self.cfg = cfg
|
| 85 |
+
self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.n_embd) # ID -> wektor
|
| 86 |
+
self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd) # pozycja
|
| 87 |
+
self.drop = nn.Dropout(cfg.dropout)
|
| 88 |
+
self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layer)])
|
| 89 |
+
self.ln_f = nn.LayerNorm(cfg.n_embd)
|
| 90 |
+
self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)
|
| 91 |
+
self.head.weight = self.tok_emb.weight # weight tying
|
| 92 |
+
self.apply(self._init)
|
| 93 |
+
|
| 94 |
+
def _init(self, m):
|
| 95 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 96 |
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
| 97 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 98 |
+
nn.init.zeros_(m.bias)
|
| 99 |
+
|
| 100 |
+
def forward(self, idx, targets=None):
|
| 101 |
+
B, T = idx.shape
|
| 102 |
+
pos = torch.arange(T, device=idx.device)
|
| 103 |
+
x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
|
| 104 |
+
for blk in self.blocks:
|
| 105 |
+
x = blk(x)
|
| 106 |
+
logits = self.head(self.ln_f(x))
|
| 107 |
+
loss = None
|
| 108 |
+
if targets is not None:
|
| 109 |
+
# cross-entropy (L03): -log p(prawdziwego następnego znaku)
|
| 110 |
+
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
|
| 111 |
+
return logits, loss
|
| 112 |
+
|
| 113 |
+
@torch.no_grad()
|
| 114 |
+
def generate(self, idx, max_new_tokens, temperature=0.8, top_k=None):
|
| 115 |
+
for _ in range(max_new_tokens):
|
| 116 |
+
idx_cond = idx[:, -self.cfg.block_size:] # przytnij do okna
|
| 117 |
+
logits, _ = self(idx_cond)
|
| 118 |
+
logits = logits[:, -1, :] / temperature
|
| 119 |
+
if top_k is not None:
|
| 120 |
+
v, _ = torch.topk(logits, top_k)
|
| 121 |
+
logits[logits < v[:, [-1]]] = float("-inf")
|
| 122 |
+
probs = F.softmax(logits, dim=-1)
|
| 123 |
+
nxt = torch.multinomial(probs, num_samples=1)
|
| 124 |
+
idx = torch.cat((idx, nxt), dim=1)
|
| 125 |
+
return idx
|
| 126 |
+
|
| 127 |
+
def num_params(self):
|
| 128 |
+
return sum(p.numel() for p in self.parameters())
|
src/make_midi.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""JEDNO POLECENIE: użyj wytrenowanego GPT, by stworzyć nowe melodie + pliki MIDI.
|
| 2 |
+
|
| 3 |
+
Przykłady:
|
| 4 |
+
python src/make_midi.py # 3 melodie w D, do data/out_*.mid
|
| 5 |
+
python src/make_midi.py --key G --n 5 # 5 melodii w G
|
| 6 |
+
python src/make_midi.py --temp 1.0 --out data/eksperyment
|
| 7 |
+
"""
|
| 8 |
+
import argparse, sys
|
| 9 |
+
import torch
|
| 10 |
+
from gpt import GPT
|
| 11 |
+
from abc_to_midi import to_midi # konwersja ABC -> MIDI (music21)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def first_tune(raw: str) -> str:
|
| 15 |
+
"""Wytnij pierwszą melodię (nagłówek + ciało aż do kolejnego 'X:')."""
|
| 16 |
+
lines = []
|
| 17 |
+
for ln in raw.split("\n"):
|
| 18 |
+
if ln.startswith("X:") and lines:
|
| 19 |
+
break
|
| 20 |
+
lines.append(ln)
|
| 21 |
+
return "\n".join(lines).strip()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
ap = argparse.ArgumentParser()
|
| 26 |
+
ap.add_argument("--key", default="D", help="tonacja, np. D, G, Am, Edor")
|
| 27 |
+
ap.add_argument("--n", type=int, default=3, help="ile melodii")
|
| 28 |
+
ap.add_argument("--temp", type=float, default=0.8, help="temperatura (więcej = śmielej)")
|
| 29 |
+
ap.add_argument("--topk", type=int, default=20)
|
| 30 |
+
ap.add_argument("--new", type=int, default=400, help="ile znaków generować na melodię")
|
| 31 |
+
ap.add_argument("--ckpt", default="data/gpt_ckpt.pt")
|
| 32 |
+
ap.add_argument("--out", default="data/out", help="prefiks plików wyjściowych")
|
| 33 |
+
args = ap.parse_args()
|
| 34 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 35 |
+
|
| 36 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 37 |
+
ck = torch.load(args.ckpt, map_location=device, weights_only=False)
|
| 38 |
+
stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
|
| 39 |
+
model = GPT(cfg).to(device)
|
| 40 |
+
model.load_state_dict(ck["model"])
|
| 41 |
+
model.eval()
|
| 42 |
+
print(f"GPT {model.num_params():,} param | val loss {ck['val_loss']:.3f} | {device}")
|
| 43 |
+
|
| 44 |
+
seed = f"X:1\nM:6/8\nK:{args.key}\n"
|
| 45 |
+
made = 0
|
| 46 |
+
for i in range(1, args.n + 1):
|
| 47 |
+
idx = torch.tensor([[stoi[c] for c in seed]], dtype=torch.long, device=device)
|
| 48 |
+
out = model.generate(idx, args.new, temperature=args.temp, top_k=args.topk)[0].tolist()
|
| 49 |
+
tune = first_tune("".join(itos[t] for t in out))
|
| 50 |
+
abc_path, mid_path = f"{args.out}_{i}.abc", f"{args.out}_{i}.mid"
|
| 51 |
+
with open(abc_path, "w", encoding="utf-8") as f:
|
| 52 |
+
f.write(tune + "\n")
|
| 53 |
+
ok = to_midi(tune, mid_path)
|
| 54 |
+
made += int(ok)
|
| 55 |
+
print(f"\n--- #{i} [{'MIDI OK -> '+mid_path if ok else 'MIDI błąd'}] ---\n{tune}")
|
| 56 |
+
|
| 57 |
+
print(f"\nGotowe: {made}/{args.n} plików MIDI ({args.out}_*.mid)")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
main()
|
src/ngram_model.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Baseline: char-level n-gram z backoffem (czysty Python, bez bibliotek).
|
| 2 |
+
Uczy się 'następny znak' z korpusu ABC i generuje nowe jigi.
|
| 3 |
+
To samo zadanie co LLM (next-token), tu na znakach i bez sieci neuronowej.
|
| 4 |
+
"""
|
| 5 |
+
import random, sys, collections
|
| 6 |
+
|
| 7 |
+
ORDER = 6 # ile znaków kontekstu (max)
|
| 8 |
+
CTX = list(range(ORDER, -1, -1)) # [6,5,4,3,2,1,0] — backoff od najdłuższego
|
| 9 |
+
|
| 10 |
+
def train(text):
|
| 11 |
+
models = {c: collections.defaultdict(collections.Counter) for c in CTX}
|
| 12 |
+
for i, ch in enumerate(text):
|
| 13 |
+
for c in CTX:
|
| 14 |
+
ctx = text[i - c:i] if c <= i else None
|
| 15 |
+
if ctx is None and c != 0:
|
| 16 |
+
continue
|
| 17 |
+
models[c][text[i - c:i]][ch] += 1
|
| 18 |
+
return models
|
| 19 |
+
|
| 20 |
+
def sample_next(models, history, temp):
|
| 21 |
+
for c in CTX: # backoff: najdłuższy znaleziony kontekst
|
| 22 |
+
ctx = history[-c:] if c > 0 else ""
|
| 23 |
+
counter = models[c].get(ctx)
|
| 24 |
+
if counter:
|
| 25 |
+
chars = list(counter)
|
| 26 |
+
weights = [counter[ch] ** (1.0 / temp) for ch in chars]
|
| 27 |
+
return random.choices(chars, weights=weights)[0]
|
| 28 |
+
return "\n"
|
| 29 |
+
|
| 30 |
+
def generate(models, seed, temp=0.8, maxlen=600, minbody=90):
|
| 31 |
+
out = seed
|
| 32 |
+
while len(out) < maxlen:
|
| 33 |
+
ch = sample_next(models, out, temp)
|
| 34 |
+
out += ch
|
| 35 |
+
body = out[len(seed):]
|
| 36 |
+
if out.endswith("\n\n") and len(body) >= minbody: # koniec melodii
|
| 37 |
+
break
|
| 38 |
+
return out.strip()
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 42 |
+
text = open("data/jigs.abc", encoding="utf-8").read()
|
| 43 |
+
print(f"trenuję n-gram (order={ORDER}) na {len(text):,} znakach…")
|
| 44 |
+
models = train(text)
|
| 45 |
+
print("gotowe. Konteksty na poziom:",
|
| 46 |
+
{c: len(models[c]) for c in CTX})
|
| 47 |
+
|
| 48 |
+
seed = "X:1\nM:6/8\nK:D\n"
|
| 49 |
+
random.seed(20260620)
|
| 50 |
+
outs = []
|
| 51 |
+
for i in range(3):
|
| 52 |
+
tune = generate(models, seed, temp=0.7)
|
| 53 |
+
outs.append(tune)
|
| 54 |
+
print(f"\n========== WYGENEROWANY JIG #{i+1} ==========")
|
| 55 |
+
print(tune)
|
| 56 |
+
|
| 57 |
+
with open("data/generated.abc", "w", encoding="utf-8") as f:
|
| 58 |
+
f.write("\n\n".join(outs) + "\n")
|
| 59 |
+
print("\n[zapisano do data/generated.abc]")
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
main()
|
src/prepare_bach.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Monofoniczny korpus Bacha (ABC) z linii SOPRANU chorałów (music21, lokalnie).
|
| 2 |
+
Chorały są 4-głosowe (SATB) — bierzemy TYLKO sopran => monofonia, zgodna z kontraktem.
|
| 3 |
+
Własny enkoder mono->ABC (music21 nie umie pisać ABC); słownik zgodny z jigami.
|
| 4 |
+
"""
|
| 5 |
+
import sys, os
|
| 6 |
+
from fractions import Fraction
|
| 7 |
+
from music21 import corpus
|
| 8 |
+
|
| 9 |
+
def pitch_to_abc(p):
|
| 10 |
+
step, octv = p.step, p.octave
|
| 11 |
+
acc = ""
|
| 12 |
+
if p.accidental is not None:
|
| 13 |
+
a = p.accidental.alter
|
| 14 |
+
acc = {1: "^", -1: "_", 0: "=", 2: "^^", -2: "__"}.get(int(a), "")
|
| 15 |
+
if octv >= 5:
|
| 16 |
+
return acc + step.lower() + "'" * (octv - 5)
|
| 17 |
+
return acc + step.upper() + "," * (4 - octv)
|
| 18 |
+
|
| 19 |
+
def dur_to_abc(ql):
|
| 20 |
+
units = Fraction(ql).limit_denominator(8) / Fraction(1, 2) # L:1/8 => ósemka=1
|
| 21 |
+
if units == 1: return ""
|
| 22 |
+
if units.denominator == 1: return str(units.numerator)
|
| 23 |
+
if units.numerator == 1: return "/" + str(units.denominator)
|
| 24 |
+
return f"{units.numerator}/{units.denominator}"
|
| 25 |
+
|
| 26 |
+
def chorale_to_abc(score):
|
| 27 |
+
sop = score.parts[0]
|
| 28 |
+
# tonacja z sygnatury (szybciej niż analyze)
|
| 29 |
+
kname = "C"
|
| 30 |
+
ks = sop.recurse().getElementsByClass('KeySignature')
|
| 31 |
+
if ks:
|
| 32 |
+
try:
|
| 33 |
+
k = ks[0].asKey()
|
| 34 |
+
kname = k.tonic.name.replace('-', 'b') + ("" if k.mode == "major" else "m")
|
| 35 |
+
except Exception:
|
| 36 |
+
pass
|
| 37 |
+
meter = "4/4"
|
| 38 |
+
ts = sop.recurse().getElementsByClass('TimeSignature')
|
| 39 |
+
if ts:
|
| 40 |
+
meter = ts[0].ratioString
|
| 41 |
+
toks = []
|
| 42 |
+
for m in sop.getElementsByClass('Measure'):
|
| 43 |
+
for el in m.notesAndRests:
|
| 44 |
+
if el.isRest:
|
| 45 |
+
toks.append("z" + dur_to_abc(el.quarterLength))
|
| 46 |
+
elif el.isChord:
|
| 47 |
+
top = max(el.notes, key=lambda x: x.pitch.midi)
|
| 48 |
+
toks.append(pitch_to_abc(top.pitch) + dur_to_abc(el.quarterLength))
|
| 49 |
+
else:
|
| 50 |
+
toks.append(pitch_to_abc(el.pitch) + dur_to_abc(el.quarterLength))
|
| 51 |
+
toks.append("|")
|
| 52 |
+
return f"X:1\nM:{meter}\nL:1/8\nK:{kname}\n" + " ".join(toks)
|
| 53 |
+
|
| 54 |
+
def main():
|
| 55 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 56 |
+
paths = corpus.getComposer('bach')
|
| 57 |
+
blocks, n = [], 0
|
| 58 |
+
for p in paths:
|
| 59 |
+
n += 1
|
| 60 |
+
try:
|
| 61 |
+
sc = corpus.parse(p)
|
| 62 |
+
if len(sc.parts) < 1:
|
| 63 |
+
continue
|
| 64 |
+
abc = chorale_to_abc(sc)
|
| 65 |
+
if 40 < len(abc) < 2500 and abc.count("|") >= 4:
|
| 66 |
+
blocks.append(abc)
|
| 67 |
+
except Exception:
|
| 68 |
+
continue
|
| 69 |
+
if len(blocks) >= 350:
|
| 70 |
+
break
|
| 71 |
+
text = "\n\n".join(blocks)
|
| 72 |
+
os.makedirs("data/corpus", exist_ok=True)
|
| 73 |
+
with open("data/corpus/bach.abc", "w", encoding="utf-8") as f:
|
| 74 |
+
f.write(text)
|
| 75 |
+
vocab = sorted(set(text))
|
| 76 |
+
print(f"przetworzono plików: {n} | zachowanych bloków (sopran): {len(blocks)}")
|
| 77 |
+
print(f"znaki: {len(text):,} | słownik: {len(vocab)}")
|
| 78 |
+
print("słownik:", repr("".join(vocab)))
|
| 79 |
+
print("\n--- pierwszy blok ---")
|
| 80 |
+
print(blocks[0] if blocks else "BRAK")
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
main()
|
src/prepare_data.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Przygotowanie korpusu ABC z thesession.org tunes.csv.
|
| 2 |
+
Filtr: typ + metrum z argumentów (domyślnie jigi 6/8). Buduje grające bloki ABC + normalizuje tonację.
|
| 3 |
+
Użycie: python src/prepare_data.py [typ] [metrum] [wyjście]
|
| 4 |
+
np. python src/prepare_data.py waltz 3/4 data/corpus/waltz.abc
|
| 5 |
+
"""
|
| 6 |
+
import csv, re, sys, os
|
| 7 |
+
csv.field_size_limit(10**7)
|
| 8 |
+
|
| 9 |
+
TYPE_KW = sys.argv[1] if len(sys.argv) > 1 else "jig"
|
| 10 |
+
METER = sys.argv[2] if len(sys.argv) > 2 else "6/8"
|
| 11 |
+
OUT = sys.argv[3] if len(sys.argv) > 3 else "data/jigs.abc"
|
| 12 |
+
|
| 13 |
+
MODE_TABLE = {
|
| 14 |
+
"major": "", "ionian": "", "minor": "min", "aeolian": "min",
|
| 15 |
+
"dorian": "dor", "mixolydian": "mix", "phrygian": "phr",
|
| 16 |
+
"lydian": "lyd", "locrian": "loc", "": "",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
def norm_key(mode: str) -> str:
|
| 20 |
+
m = re.match(r"^([A-Ga-g][#b]?)(.*)$", mode.strip())
|
| 21 |
+
if not m:
|
| 22 |
+
return "C"
|
| 23 |
+
root, word = m.group(1), m.group(2).lower()
|
| 24 |
+
return root + MODE_TABLE.get(word, "")
|
| 25 |
+
|
| 26 |
+
ALLOWED = set("ABCDEFGabcdefg0123456789|:[]()<>/'^_=.,~- zZxX")
|
| 27 |
+
|
| 28 |
+
def clean_abc(body: str) -> str:
|
| 29 |
+
body = body.replace("\r\n", "\n").replace("\r", "\n").strip()
|
| 30 |
+
body = re.sub(r'"[^"]*"', "", body) # usuń symbole akordów / adnotacje "..."
|
| 31 |
+
body = re.sub(r"[ \t]+", " ", body) # scal podwójne spacje po usunięciu
|
| 32 |
+
body = re.sub(r"\n+", "\n", body)
|
| 33 |
+
return body
|
| 34 |
+
|
| 35 |
+
def main():
|
| 36 |
+
rows_out, n_total, n_kept = [], 0, 0
|
| 37 |
+
with open("data/tunes.csv", encoding="utf-8", newline="") as f:
|
| 38 |
+
for row in csv.DictReader(f):
|
| 39 |
+
n_total += 1
|
| 40 |
+
if TYPE_KW not in row["type"].lower():
|
| 41 |
+
continue
|
| 42 |
+
if row["meter"].strip() != METER:
|
| 43 |
+
continue
|
| 44 |
+
body = clean_abc(row["abc"])
|
| 45 |
+
if not (40 <= len(body) <= 700):
|
| 46 |
+
continue
|
| 47 |
+
if any(ch not in ALLOWED for ch in body.replace("\n", "")):
|
| 48 |
+
continue
|
| 49 |
+
key = norm_key(row["mode"])
|
| 50 |
+
block = f"X:1\nM:{METER}\nK:{key}\n{body}\n"
|
| 51 |
+
rows_out.append(block)
|
| 52 |
+
n_kept += 1
|
| 53 |
+
|
| 54 |
+
text = "\n".join(rows_out)
|
| 55 |
+
os.makedirs(os.path.dirname(OUT) or ".", exist_ok=True)
|
| 56 |
+
with open(OUT, "w", encoding="utf-8") as f:
|
| 57 |
+
f.write(text)
|
| 58 |
+
|
| 59 |
+
vocab = sorted(set(text))
|
| 60 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 61 |
+
print(f"melodii w pliku : {n_total}")
|
| 62 |
+
print(f"{TYPE_KW} ({METER}) zachowane: {n_kept}")
|
| 63 |
+
print(f"znaki łącznie : {len(text):,}")
|
| 64 |
+
print(f"słownik ({len(vocab)}) : {''.join(vocab)!r}")
|
| 65 |
+
print("\n--- pierwszy blok ---")
|
| 66 |
+
print(rows_out[0] if rows_out else "BRAK")
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
src/sample_gpt.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generacja melodii z wytrenowanego GPT (wczytuje checkpoint)."""
|
| 2 |
+
import sys
|
| 3 |
+
import torch
|
| 4 |
+
from gpt import GPT
|
| 5 |
+
|
| 6 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 7 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 8 |
+
ck = torch.load("data/gpt_ckpt.pt", map_location=device, weights_only=False)
|
| 9 |
+
stoi, itos, cfg = ck["stoi"], ck["itos"], ck["config"]
|
| 10 |
+
|
| 11 |
+
model = GPT(cfg).to(device)
|
| 12 |
+
model.load_state_dict(ck["model"])
|
| 13 |
+
model.eval()
|
| 14 |
+
print(f"wczytano GPT ({model.num_params():,} param) | val loss {ck['val_loss']:.3f}")
|
| 15 |
+
|
| 16 |
+
def gen(seed, n_new=400, temp=0.8, top_k=20):
|
| 17 |
+
idx = torch.tensor([[stoi[c] for c in seed]], dtype=torch.long, device=device)
|
| 18 |
+
out = model.generate(idx, n_new, temperature=temp, top_k=top_k)[0].tolist()
|
| 19 |
+
return "".join(itos[i] for i in out)
|
| 20 |
+
|
| 21 |
+
seed = "X:1\nM:6/8\nK:D\n"
|
| 22 |
+
raw = gen(seed, n_new=500, temp=0.8, top_k=20)
|
| 23 |
+
# potnij na osobne melodie po pustej linii
|
| 24 |
+
tunes, cur = [], []
|
| 25 |
+
for line in raw.split("\n"):
|
| 26 |
+
if line.startswith("X:") and cur:
|
| 27 |
+
tunes.append("\n".join(cur)); cur = []
|
| 28 |
+
cur.append(line)
|
| 29 |
+
if cur:
|
| 30 |
+
tunes.append("\n".join(cur))
|
| 31 |
+
tunes = [t.strip() for t in tunes if "X:" in t][:5]
|
| 32 |
+
|
| 33 |
+
for i, t in enumerate(tunes, 1):
|
| 34 |
+
print(f"\n===== GPT JIG #{i} =====\n{t}")
|
| 35 |
+
with open("data/generated_gpt.abc", "w", encoding="utf-8") as f:
|
| 36 |
+
f.write("\n\n".join(tunes) + "\n")
|
| 37 |
+
print("\n[zapisano -> data/generated_gpt.abc]")
|
src/train_gpt.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trening GPT od zera na korpusie ABC (L03/L08 w praktyce).
|
| 2 |
+
Batche -> strata cross-entropy -> backprop -> AdamW -> val loss -> checkpoint.
|
| 3 |
+
"""
|
| 4 |
+
import os, time, math, sys
|
| 5 |
+
from contextlib import nullcontext
|
| 6 |
+
import torch
|
| 7 |
+
from gpt import GPT, GPTConfig
|
| 8 |
+
|
| 9 |
+
# --- hiperparametry ---
|
| 10 |
+
block_size = 128
|
| 11 |
+
batch_size = 32
|
| 12 |
+
n_layer = 4
|
| 13 |
+
n_head = 4
|
| 14 |
+
n_embd = 128
|
| 15 |
+
dropout = 0.1
|
| 16 |
+
lr = 3e-4
|
| 17 |
+
max_iters = 2000
|
| 18 |
+
eval_interval = 200
|
| 19 |
+
eval_iters = 100
|
| 20 |
+
warmup = 100
|
| 21 |
+
|
| 22 |
+
torch.manual_seed(20260620)
|
| 23 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 24 |
+
use_bf16 = device == "cuda" and torch.cuda.is_bf16_supported()
|
| 25 |
+
ctx = torch.autocast(device_type="cuda", dtype=torch.bfloat16) if use_bf16 else nullcontext()
|
| 26 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 27 |
+
print(f"urządzenie: {device} | bf16: {use_bf16}")
|
| 28 |
+
|
| 29 |
+
# --- ścieżki z argumentów (domyślnie: jigi) ---
|
| 30 |
+
DATA = sys.argv[1] if len(sys.argv) > 1 else "data/jigs.abc"
|
| 31 |
+
CKPT = sys.argv[2] if len(sys.argv) > 2 else "data/gpt_ckpt.pt"
|
| 32 |
+
LOSSLOG = sys.argv[3] if len(sys.argv) > 3 else "data/loss_log.csv"
|
| 33 |
+
VOCAB_FROM = sys.argv[4] if len(sys.argv) > 4 else None # wspólny słownik z innego ckpt (do stitchu)
|
| 34 |
+
print(f"dane: {DATA} -> checkpoint: {CKPT}")
|
| 35 |
+
|
| 36 |
+
# --- dane: char-level ---
|
| 37 |
+
text = open(DATA, encoding="utf-8").read()
|
| 38 |
+
if VOCAB_FROM:
|
| 39 |
+
vck = torch.load(VOCAB_FROM, map_location="cpu", weights_only=False)
|
| 40 |
+
stoi, itos = vck["stoi"], vck["itos"]
|
| 41 |
+
chars = [itos[i] for i in range(len(itos))]
|
| 42 |
+
print(f"wspólny słownik z {VOCAB_FROM}: {len(chars)} znaków")
|
| 43 |
+
else:
|
| 44 |
+
chars = sorted(set(text))
|
| 45 |
+
stoi = {c: i for i, c in enumerate(chars)}
|
| 46 |
+
itos = {i: c for i, c in enumerate(chars)}
|
| 47 |
+
data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
|
| 48 |
+
n = int(0.9 * len(data))
|
| 49 |
+
train_data, val_data = data[:n], data[n:]
|
| 50 |
+
print(f"słownik: {len(chars)} | tokeny: train {len(train_data):,} / val {len(val_data):,}")
|
| 51 |
+
|
| 52 |
+
def get_batch(split):
|
| 53 |
+
d = train_data if split == "train" else val_data
|
| 54 |
+
ix = torch.randint(len(d) - block_size, (batch_size,))
|
| 55 |
+
x = torch.stack([d[i:i+block_size] for i in ix])
|
| 56 |
+
y = torch.stack([d[i+1:i+1+block_size] for i in ix])
|
| 57 |
+
return x.to(device), y.to(device)
|
| 58 |
+
|
| 59 |
+
@torch.no_grad()
|
| 60 |
+
def estimate_loss():
|
| 61 |
+
model.eval()
|
| 62 |
+
out = {}
|
| 63 |
+
for split in ("train", "val"):
|
| 64 |
+
losses = torch.zeros(eval_iters)
|
| 65 |
+
for k in range(eval_iters):
|
| 66 |
+
x, y = get_batch(split)
|
| 67 |
+
with ctx:
|
| 68 |
+
_, loss = model(x, y)
|
| 69 |
+
losses[k] = loss.item()
|
| 70 |
+
out[split] = losses.mean().item()
|
| 71 |
+
model.train()
|
| 72 |
+
return out
|
| 73 |
+
|
| 74 |
+
def lr_at(it): # warmup + cosine decay (L08)
|
| 75 |
+
if it < warmup:
|
| 76 |
+
return lr * it / warmup
|
| 77 |
+
r = (it - warmup) / (max_iters - warmup)
|
| 78 |
+
return lr * 0.1 + 0.5 * lr * 0.9 * (1 + math.cos(math.pi * r))
|
| 79 |
+
|
| 80 |
+
cfg = GPTConfig(vocab_size=len(chars), block_size=block_size,
|
| 81 |
+
n_layer=n_layer, n_head=n_head, n_embd=n_embd, dropout=dropout)
|
| 82 |
+
model = GPT(cfg).to(device)
|
| 83 |
+
print(f"parametry modelu: {model.num_params():,}")
|
| 84 |
+
opt = torch.optim.AdamW(model.parameters(), lr=lr, betas=(0.9, 0.99), weight_decay=0.1)
|
| 85 |
+
|
| 86 |
+
best_val = float("inf")
|
| 87 |
+
log = [("iter", "train_loss", "val_loss")]
|
| 88 |
+
t0 = time.time()
|
| 89 |
+
for it in range(max_iters + 1):
|
| 90 |
+
if it % eval_interval == 0 or it == max_iters:
|
| 91 |
+
L = estimate_loss()
|
| 92 |
+
ppl = math.exp(L["val"])
|
| 93 |
+
print(f"iter {it:4d} | train {L['train']:.3f} | val {L['val']:.3f} | ppl {ppl:.2f} | {time.time()-t0:.0f}s")
|
| 94 |
+
log.append((it, round(L["train"], 4), round(L["val"], 4)))
|
| 95 |
+
if L["val"] < best_val: # zapis najlepszego (early-stop logic)
|
| 96 |
+
best_val = L["val"]
|
| 97 |
+
torch.save({"model": model.state_dict(), "config": cfg,
|
| 98 |
+
"stoi": stoi, "itos": itos, "val_loss": best_val},
|
| 99 |
+
CKPT)
|
| 100 |
+
if it == max_iters:
|
| 101 |
+
break
|
| 102 |
+
for g in opt.param_groups:
|
| 103 |
+
g["lr"] = lr_at(it)
|
| 104 |
+
x, y = get_batch("train")
|
| 105 |
+
with ctx:
|
| 106 |
+
_, loss = model(x, y)
|
| 107 |
+
opt.zero_grad(set_to_none=True)
|
| 108 |
+
loss.backward()
|
| 109 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 110 |
+
opt.step()
|
| 111 |
+
|
| 112 |
+
with open(LOSSLOG, "w", encoding="utf-8") as f:
|
| 113 |
+
f.write("\n".join(",".join(map(str, row)) for row in log))
|
| 114 |
+
print(f"\ngotowe. best val loss: {best_val:.3f} (ppl {math.exp(best_val):.2f})")
|
| 115 |
+
print(f"checkpoint -> {CKPT} | krzywa -> {LOSSLOG}")
|
src/upload_hf.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Upload paczki modelu na Hugging Face Hub.
|
| 2 |
+
Wymaga wcześniejszego `huggingface-cli login`. Uruchom z katalogu repo:
|
| 3 |
+
python src/upload_hf.py Maggio33/slay-piano-gpt
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
from huggingface_hub import HfApi, create_repo
|
| 7 |
+
|
| 8 |
+
IGNORE = [
|
| 9 |
+
"data/tunes.csv", "data/jigs.abc", "data/gpt_ckpt_v1_dirty.pt",
|
| 10 |
+
"data/_test_chord.mid", "data/generated*.abc",
|
| 11 |
+
"data/demo_*.mid", "data/clean_*.mid", "data/gpt_jig_*.mid",
|
| 12 |
+
"data/jig_*.mid", "data/out_*.*",
|
| 13 |
+
"**/__pycache__/**", "*.pyc",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
repo_id = sys.argv[1] if len(sys.argv) > 1 else "Maggio33/slay-piano-gpt"
|
| 18 |
+
api = HfApi()
|
| 19 |
+
who = api.whoami()
|
| 20 |
+
print("zalogowany jako:", who.get("name"))
|
| 21 |
+
create_repo(repo_id, repo_type="model", exist_ok=True, private=False)
|
| 22 |
+
print(f"repo gotowe: {repo_id} — wysyłam pliki…")
|
| 23 |
+
api.upload_folder(
|
| 24 |
+
folder_path=".",
|
| 25 |
+
repo_id=repo_id,
|
| 26 |
+
repo_type="model",
|
| 27 |
+
ignore_patterns=IGNORE,
|
| 28 |
+
commit_message="Initial: slay-piano-gpt (0.82M char-level GPT, jigs ABC)",
|
| 29 |
+
)
|
| 30 |
+
print(f"\nGOTOWE -> https://huggingface.co/{repo_id}")
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
main()
|