Add a narrative 'development chronicle' to the report notebook, drawn from the project's commit history: six instructive episodes (BAR-token removal, key normalization, the shared-bias aliasing bug, --bars vs early EOS, fail-loud on empty bars, fine-tune lr/epoch search) plus three short notes (open style tags, corpus hygiene, the 'time' name-shadowing trap). Each episode ends with a generalizable lesson. Renumber the conclusions section from 7 to 8 accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
308 KiB
hamori — отчёт по проекту¶
Автор: Владислав Аршинов, группа БСМО-11-25 Дата: июнь 2026
Тема работы — генерация коротких аккордовых последовательностей (я называю их «гармоническими периодами», 4–16 тактов) в моём композиторском стиле с помощью небольшого трансформера.
Название hamori (яп. ハモリ — «вторить, петь вторым голосом») отражает идею проекта: модель не сочиняет музыку с нуля, а предлагает гармонию к уже задуманной композитором линии — добавляет к ней второй голос.
Ноутбук одновременно служит отчётом: текст, код и графики собраны в одном файле,
чтобы результаты считались на месте. Для полного прогона нужны обученные
чекпоинты в checkpoints/ и данные в data/.
0. Постановка задачи¶
Я генерирую не целую песню, а один законченный гармонический период — короткий
замкнутый фрагмент гармонии на 4–16 тактов. В такте несколько позиций (их число
задаёт subdivision); в позиции может стоять аккорд, точка . («держать
предыдущий»), NC (без аккорда) или ? (не разобрано).
Конвейер устроен так:
- Переношу свои композиции из проектов REAPER в текстовый формат
.chord. - Паршу
.chordи перевожу в последовательности токенов. - Предобучаю модель на большом внешнем корпусе (McGill Billboard).
- Дообучаю на собственном корпусе.
- Сэмплирую новые периоды, задавая лад, размер, стиль, функцию и, при желании, начальные аккорды.
- Перевожу результат обратно в
.chordи в MIDI для REAPER.
Основная гипотеза, которую я проверяю: дообучение на небольшом авторском корпусе сдвигает генерации ближе к моему стилю, не разрушая при этом общие закономерности гармонии, выученные на большом корпусе. Проверяю это тремя способами — перплексией, сравнением распределений и разбором конкретных примеров.
1. Окружение¶
Ячейка ниже находит корень репозитория (папку с src/), добавляет его в
sys.path, фиксирует random seed для воспроизводимости и импортирует модули
проекта. Основную логику — парсинг, токенизацию, расчёт перплексии, генерацию —
я не дублирую в ноутбуке, а беру готовую из src/.
import sys, csv, random
from pathlib import Path
import numpy as np
import torch
import matplotlib.pyplot as plt
from IPython.display import Image, display
# Найти корень репозитория (каталог, содержащий src/).
ROOT = Path.cwd()
while not (ROOT / "src").exists() and ROOT != ROOT.parent:
ROOT = ROOT.parent
sys.path.insert(0, str(ROOT))
SEED = 42
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
from src.tokenizer import VOCAB, ID_TO_TOKEN, parse_chord_file, tokenize_period
from src.model import ChordTransformer
from src.dataset import ChordDataset
from src.evaluate import compute_perplexity
from src.generate import generate_period
# src.evaluate forces the headless 'Agg' backend on import (used by the CLI to
# save PNGs); re-enable the inline backend so figures render in this notebook.
%matplotlib inline
CKPT = ROOT / "checkpoints"
DATA = ROOT / "data"
OUT = ROOT / "output" / "eval"
print(f"ROOT = {ROOT}")
print(f"device = {DEVICE}")
print(f"vocab = {len(VOCAB)} токенов")
2. Данные¶
Формат .chord¶
Один файл — один период. Сверху заголовок (строки с #): title, key,
time, subdivision, style, function. Дальше тело — такты через |, по
subdivision позиций в каждом.
Аккорд записывается как <тоника><качество?><надстройка?>(/<бас>)?. Качеств 18,
надстроек 7, обращения (через слеш, например C/E) — обязательная часть записи:
для моей музыки линия баса важна, поэтому я её не отбрасываю.
Нормализация тональности¶
Перед токенизацией я транспонирую весь период: мажор → в C-dur, минор → в
a-moll. Так модель не видит абсолютную тональность — только лад (MODE_major /
MODE_minor). Это сделано намеренно: периодов всего 68, и они в разных
тональностях. Если свести их к одной, все они учат одну и ту же гармоническую
«грамматику», а не размазываются по 24 тональностям. После генерации результат
транспонируется обратно в нужную тональность.
Токенизация¶
Каждый новый аккорд кодируется ровно 4 токенами: ROOT_* (тоника), QUAL_*
(качество), EXT_* (надстройка), BASS_* (бас). Удержание — один токен HOLD.
Границы тактов токенами не являются: при обратной сборке они восстанавливаются
подсчётом позиций (time × subdivision). Плюс метатокены в начале. Итоговый
словарь — 84 токена.
user_files = sorted((DATA / "raw_user" / "H1K0").glob("*.chord"))
mcgill_train = sorted((DATA / "processed" / "mcgill" / "train").glob("*.pt"))
user_train = sorted((DATA / "processed" / "user" / "train").glob("*.pt"))
user_val = sorted((DATA / "processed" / "user" / "val").glob("*.pt"))
print(f"Корпус автора (.chord) : {len(user_files)} периодов")
print(f" → train / val : {len(user_train)} / {len(user_val)}")
print(f"McGill (предобучение) : {len(mcgill_train)} периодов (train-сплит)")
Ниже — один период «как есть», сырой текст файла .chord:
example = DATA / "raw_user" / "H1K0" / "celestial_sphere-chorus.chord"
print(example.read_text(encoding="utf-8"))
Тот же период после парсинга и токенизации. Здесь видна нормализация, о
которой я писал выше: в файле тональность Eb_major, но первые токены —
MODE_major (период уже переведён в C-dur), а первый аккорд Eb стал ROOT_C.
Абсолютная тональность исчезла, остался только лад.
period = parse_chord_file(example)
ids = tokenize_period(period)
toks = [ID_TO_TOKEN[i] for i in ids]
print(f"Тональность в файле : {period.key}")
print(f"Тактов : {len(period.bars)}")
print(f"Токенов всего : {len(ids)}")
print()
print("Первые 14 токенов (метаданные + первый аккорд):")
print(" " + " ".join(toks[:14]))
3. Модель¶
Я использую небольшой авторегрессионный трансформер — по сути ту же архитектуру, что и в языковых моделях, только вместо слов здесь аккордовые токены.
Принцип работы¶
Модель учится предсказывать следующий токен по всем предыдущим: она получает начало периода и оценивает, какой токен идёт дальше; ошибка считается кросс-энтропией.
Подробнее о том, что происходит внутри:
- Эмбеддинги. Каждый из 84 токенов превращается в вектор длиной
d_model. К нему добавляется позиционная информация — без неё модель не различала бы порядок токенов. - Self-attention. Основной механизм трансформера. На каждой позиции модель «смотрит» на остальные позиции последовательности и определяет, какие из них сейчас значимы. Например, выдавая очередной аккорд, она может учесть тонику в начале периода и предыдущий аккорд.
- Причинная (causal) маска. Чтобы при обучении модель не использовала будущие токены, внимание ограничено «назад»: токен на позиции i видит только позиции до i включительно. Без этого задача предсказания следующего токена была бы тривиальной — ответ виден заранее.
- FFN и слои. После внимания идёт полносвязный блок, и так несколько раз (2–4 слоя). Каждый слой позволяет уловить более сложные зависимости.
- Выход. На последнем слое модель выдаёт распределение вероятностей по всем 84 токенам — насколько вероятен каждый следующим. При генерации я сэмплирую из этого распределения (раздел 6).
Входные и выходные эмбеддинги связаны (tied embeddings): это экономит параметры и обычно немного помогает на небольших данных.
Конкретные размеры (число слоёв, d_model, число голов) хранятся в чекпоинте —
выведу их и посчитаю число параметров. Модель небольшая и обучается даже на
CPU.
def load_model(path: Path):
ckpt = torch.load(path, map_location=DEVICE, weights_only=True)
model = ChordTransformer(**ckpt["model_config"])
model.load_state_dict(ckpt["model_state"])
model.to(DEVICE).eval()
return model, ckpt["model_config"]
pretrained, cfg = load_model(CKPT / "pretrained.pt")
finetuned, _ = load_model(CKPT / "finetuned.pt")
n_params = sum(p.numel() for p in finetuned.parameters())
print("Конфигурация модели:")
for k, v in cfg.items():
print(f" {k:18s} = {v}")
print(f"\nПараметров (с привязанными эмбеддингами): {n_params:,}")
4. Обучение¶
Обучение двухэтапное:
- Предобучение на McGill Billboard. На большом внешнем корпусе модель усваивает общие закономерности: как движется гармония, какие бывают надстройки, насколько часто встречаются обращения. Мой стиль здесь ни при чём — задача в том, чтобы модель в принципе «понимала» гармонию.
- Дообучение на собственном корпусе. Здесь я ставлю заметно меньший learning rate (порядка 3·10⁻⁵ против 3·10⁻⁴ на предобучении) и небольшое число эпох. Цель — сместить модель в сторону моего стиля, но не дать ей забыть выученное на большом корпусе (катастрофическое забывание).
Кривые потерь беру из CSV-логов, сохранённых тренировочным скриптом.
def load_log(path: Path):
e, tr, vl, pp = [], [], [], []
with open(path, encoding="utf-8") as fh:
for row in csv.DictReader(fh):
e.append(int(row["epoch"]))
tr.append(float(row["train_loss"]))
vl.append(float(row["val_loss"]))
pp.append(float(row["val_ppl"]))
return e, tr, vl, pp
fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))
for ax, name, title in [
(axes[0], "pretrained", "Предобучение (McGill)"),
(axes[1], "finetuned", "Дообучение (корпус автора)"),
]:
e, tr, vl, pp = load_log(CKPT / f"{name}.log.csv")
ax.plot(e, tr, label="train loss")
ax.plot(e, vl, label="val loss")
ax.set_xlabel("эпоха"); ax.set_ylabel("loss")
ax.set_title(f"{title} · лучшая val-PPL = {min(pp):.2f}")
ax.grid(alpha=0.3); ax.legend()
fig.tight_layout(); plt.show()
Предобучение сошлось к val-перплексии около 1.32 за 50 эпох — McGill большой и довольно регулярный корпус, поэтому перплексия низкая. Дообучение вышло на плато около 2.15 на моей валидации (лучшая эпоха — 20-я). Более высокая перплексия после дообучения ожидаема: мой корпус меньше и стилистически своеобразнее, предсказывать его сложнее.
5. Оценка¶
Гипотезу я проверяю с трёх сторон:
- Перплексия — насколько хорошо модель предсказывает мои отложенные периоды.
- Распределения — гистограммы по качествам аккордов, надстройкам, обращениям и интервалам между тониками; по ним видно, в какую сторону дообучение двигает генерации.
- Примеры — одно и то же условие, генерация до и после дообучения, сравнение вручную.
О слабом месте эксперимента. Папку
data/holdout/в этой версии я не заполнил, поэтому перплексия считается на валидационном сплите (7 периодов), а не на отдельном тестовом наборе. Валидация косвенно участвовала в выборе эпохи, так что числа стоит читать как направление и порядок величины, а не как строгую несмещённую оценку. При наличии времени я заполню holdout и пересчитаю.
5.1 Перплексия¶
Считаю вживую, на своей валидации, сразу для обеих моделей.
loader = torch.utils.data.DataLoader(
ChordDataset(DATA / "processed" / "user" / "val",
max_length=pretrained.max_seq_len),
batch_size=8, shuffle=False, num_workers=0,
)
ppl_pre = compute_perplexity(pretrained, loader, DEVICE)
ppl_ft = compute_perplexity(finetuned, loader, DEVICE)
improve = (ppl_pre - ppl_ft) / ppl_pre * 100
print(f"Перплексия на валидации автора ({len(user_val)} периодов):")
print(f" pretrained = {ppl_pre:.2f}")
print(f" finetuned = {ppl_ft:.2f}")
print(f" улучшение = {improve:+.1f}%")
fig, ax = plt.subplots(figsize=(4.6, 4))
bars = ax.bar(["pretrained", "finetuned"], [ppl_pre, ppl_ft],
color=["#999999", "#3b7dd8"])
for rect, v in zip(bars, [ppl_pre, ppl_ft]):
ax.text(rect.get_x() + rect.get_width() / 2, v + 0.03,
f"{v:.2f}", ha="center", va="bottom")
ax.set_ylabel("перплексия (ниже — лучше)")
ax.set_title("Перплексия: pretrained vs finetuned")
ax.grid(axis="y", alpha=0.3)
plt.show()
5.2 Распределения¶
Картинку строит скрипт scripts/evaluate.py: он сравнивает четыре группы —
McGill (предобучение), мой корпус (цель), выход модели до дообучения и после.
Панели:
- Chord quality — распределение качеств аккордов;
- Root motion intervals — интервал между тониками соседних аккордов (0–11 полутонов);
- Extension presence — доля аккордов с надстройками;
- Inversion frequency — доля обращений (бас не тоника).
Ниже — готовая картинка output/eval/distributions.png. Команда для пересборки:
python scripts/evaluate.py --pretrained checkpoints/pretrained.pt \
--finetuned checkpoints/finetuned.pt
Ожидаемый результат: группа finetuned ближе к «моему корпусу», чем pretrained — прежде всего по обращениям и надстройкам, которые я использую часто.
dist_png = OUT / "distributions.png"
if dist_png.exists():
display(Image(str(dist_png)))
else:
print("Файл не найден. Сгенерируйте его командой:")
print(" python scripts/evaluate.py --pretrained checkpoints/pretrained.pt "
"--finetuned checkpoints/finetuned.pt")
Как читать график. Цвета: синий — McGill (корпус предобучения), оранжевый — мой корпус (цель), зелёный — выход pretrained, красный — выход finetuned. Чем ближе красный к оранжевому, тем лучше дообучение поймало мой стиль.
Что видно по факту:
- Движение тоник (панель 2) — главный эффект. До дообучения pretrained
(зелёный) почти в половине случаев топчется на той же тонике (
P1≈ 43 %); после дообучения finetuned (красный) спускается до ≈ 18 %, почти как мой корпус (≈ 14 %), и набирает шаги на тон (M2). Гармония стала заметно подвижнее. - Качества аккордов (панель 1) — finetuned срезал доминантсептаккорды (
7) почти до моего уровня, но перекосил в сторону «голого»maj. - Обращения и надстройки (панели 3–4) — здесь моё ожидание не подтвердилось: finetuned их, наоборот, недодаёт (обращений ≈ 3 % против моих ≈ 9 %). Скорее всего, корпуса слишком мало, чтобы модель уверенно переняла мою привычку к slash-аккордам.
5.3 Качественные примеры¶
Беру одно и то же условие (тональность, размер, стиль, функция, seed) и генерирую двумя моделями. Эталон — мой собственный период, на метаданные которого я опираюсь.
def one_line(path: Path) -> str:
p = parse_chord_file(path)
return "| " + " | ".join(" ".join(bar) for bar in p.bars) + " |"
ref = DATA / "raw_user" / "H1K0" / "celestial_sphere-chorus.chord"
pre = OUT / "examples" / "pretrained_01_celestial_sphere-chorus.chord"
ftn = OUT / "examples" / "finetuned_01_celestial_sphere-chorus.chord"
print("Эталон (автор):")
print(" ", one_line(ref))
print("\npretrained:")
print(" ", one_line(pre))
print("\nfinetuned:")
print(" ", one_line(ftn))
Что видно на этом примере (celestial_sphere, припев, Eb-dur):
- Обе модели держатся в тональности — правда, генерация заякорена на тонику, так что это отчасти заслуга условия, а не модели.
- Энгармония. Модель выдаёт диезы (
D#maj,G#maj,A#maj), а я тот же звук записал бемолями (Eb,Ab,Bb). Это не ошибка: в словаре все 12 ступеней хранятся только в диезной записи (ROOT_*— sharps only), и при обратной сборке выбирается диезное имя. Звучит идентично; при желании имена можно перевести в бемоли отдельной пост-обработкой. - finetuned движется заметнее и ближе к моему стилю (тоника → субдоминанта → доминанта → тоника), тогда как pretrained чаще повторяет тонику. Это всего один пример, поэтому вывод осторожный.
6. Генерация¶
Сэмплирую через nucleus / top-p (≈ 0.9) с температурой около 1.0. Beam search не использую — на генеративных задачах он обычно ухудшает результат, делая его однообразным. Дополнительные параметры:
- tonic anchor — по умолчанию ставлю в начало тонику, чтобы генерация не уходила из тональности;
- repetition penalty — биграммный штраф за повтор переходов между тониками (по умолчанию выключен, разумный диапазон 0.5–1.5);
- n_bars — позволяет зафиксировать точное число тактов.
Запустить генерацию можно тремя способами: из командной строки
(scripts/generate.py), через веб-форму (app.py, небольшой Gradio) или
напрямую вызовом generate_period(...). Ниже — последний вариант.
period = generate_period(
model=finetuned,
mode="major", key="C_major",
time="4/4", subdivision=4,
style="H1K0", function="chorus",
prefix=["C"], # якорь на тонику
temperature=1.0, top_p=0.9,
repetition_penalty=0.8, # сбиваю залипание на I–V
n_bars=8, seed=4,
)
print(f"Сгенерировано ({len(period.bars)} тактов, тональность {period.key}):")
print("| " + " | ".join(" ".join(bar) for bar in period.bars) + " |")
7. Хроника разработки: на чём я спотыкался¶
Готовый конвейер выглядит аккуратно, но дорога к нему была неровной. Самые поучительные моменты я собрал ниже — не ради полноты, а потому что каждый из них чему-то научил. Все эпизоды восстановлены по истории коммитов, так что это не воспоминания, а задокументированные факты.
7.1 Лишний токен на границе такта¶
В первой версии формата граница такта (|) была отдельным токеном BAR в
словаре. Логично же: раз в тексте есть разделитель — пусть будет и в
последовательности. Но довольно быстро выяснилось, что это плохая идея. Граница
такта полностью определяется арифметикой: на такт приходится ровно
time × subdivision позиций, и где она стоит, можно вычислить, ничего не
предсказывая. А раз так, то токен BAR не несёт информации — зато даёт модели
лишнюю возможность ошибиться: поставить границу не туда и выдать кривой такт.
Я убрал BAR из словаря (стало 84 токена вместо 85), а детокенизатор теперь
расставляет границы сам, подсчётом позиций; генератор же разрешает EOS только
на границе такта. Урок простой и общий: не заставляйте модель учить то, что и
так гарантировано детерминированным правилом — пусть тратит ёмкость на
осмысленные предсказания. Заодно это потребовало поднять версию спецификации
формата до v2.3: формат — контракт между данными и моделью, и менять его молча
нельзя.
7.2 Двадцать четыре тональности на шестьдесят восемь периодов¶
Корпус у меня крошечный — 68 периодов, и записаны они в самых разных тональностях. Если оставить как есть, модель увидела бы один и тот же оборот I–IV–V в C-dur и в Es-dur как две совершенно несвязанные цепочки токенов — и так по всем двенадцати мажорам и минорам. Данные, и без того скудные, размазались бы ещё в двадцать четыре раза.
Поэтому нормализация тональности (подробно описана в разделе 2) заложена в формат
с самого начала: всё сводится в C-dur / a-moll, лад остаётся в виде
MODE_major / MODE_minor, а после генерации результат транспонируется обратно.
Это не оптимизация и не тонкая настройка — это решение, которое на таком объёме
данных дало больше, чем любые правки самой модели. Урок: когда данных мало,
выгоднее убрать мешающее измерение, чем усложнять модель.
7.3 Качество, утекающее между генерациями¶
Самая поучительная история — и самая коварная. Через веб-форму при повторных
генерациях качество выхода заметно падало: модель всё чаще выдавала точки и NC,
и в какой-то момент при любых параметрах не выдавала ничего, кроме них.
Перезапуск сервера помогал лишь отчасти.
Сначала это сбивало с толку. Веса модели лежат на диске и не меняются; параметры сэмплирования на каждый запрос свои. Что вообще могло «накапливаться» между независимыми вызовами? Ключом оказалась как раз формулировка симптома: ухудшается от вызова к вызову, сбрасывается при перезапуске — это почерк испорченного состояния в памяти процесса, живущего дольше одного запроса.
Виновником были тензоры грамматических смещений — те самые маски, которые
запрещают модели выдавать недопустимые в данной позиции токены. Они создаются
один раз при импорте как общие объекты-синглтоны. А цикл генерации правил их
на месте: и блокировку EOS, и биграммный штраф за повтор он применял прямо
к возвращённой ссылке. В результате штраф накапливался по позициям внутри одного
вызова (выход схлопывался в удержания и NC) и протекал в следующие вызовы,
пока процесс не перезапустят. Один и тот же корень у обоих симптомов — типичный
для Python алиасинг общего изменяемого состояния.
Коварство в том, что фиксированный seed маскировал проблему: отдельный прогон
оставался воспроизводимым, и в тестах всё выглядело нормально. Баг вылезал только
под реальным повторным использованием. Починка свелась к одной строке — копировать
смещение (.clone()) на каждом шаге, прежде чем что-то в нём менять. Но прежде
фикса я, по правилу проекта, написал сначала падающий тест: он проверяет, что
синглтоны после генерации не изменились и что один и тот же seed даёт один и тот
же результат независимо от того, сколько генераций было до. На баговом коде он
падал, после фикса — зелёный.
7.4 Модель, которая хотела закончить слишком рано¶
После дообучения на коротких периодах модель усвоила не только мой стиль, но и
привычку быстро ставить точку: просишь восемь тактов — а она норовит закруглиться
на четвёртом, уверенно выдавая EOS. Для генеративной модели это нормально, но
пользователю нужна заданная длина.
Я добавил параметр --bars, а затем научил его подавлять EOS (занулять
вероятность этого токена маской логитов) до тех пор, пока не набрано нужное число
тактов. Урок: стоит развести две разные вещи — «модель хочет остановиться» и
«пользователь задал длину N». Управляемость достигается маскированием логитов
поверх вероятностной генерации, а не переучиванием модели.
7.5 Лучше упасть, чем тихо испортить¶
При обратной сборке вырожденная цепочка токенов иногда могла дать пустой такт.
Заманчиво было просто молча его выкинуть — и идти дальше. Но я сделал наоборот:
детокенизатор бросает ChordFormatError с указанием места. Это прямо следует из
принципа проекта — никогда молча не «подправлять» неразобранное.
Урок касается именно обучающих данных: молчаливая порча — худший режим отказа. Тихо проглоченная ошибка всплывёт через недели в виде необъяснимо плохих генераций, и искать её придётся с нуля; громкое падение с координатами (файл, такт, позиция) экономит эти недели.
7.6 Подбор режима дообучения¶
Дообучение пришлось настраивать на ощупь, между двумя крайностями. Слишком большой learning rate или слишком много эпох — и модель забывает гармонические закономерности, выученные на McGill (катастрофическое забывание). Слишком осторожно — и стиль не сдвигается вовсе. Первый прогон был аккуратным (lr = 1·10⁻⁵, 50 эпох), затем я сравнил его с более бодрым (lr = 3·10⁻⁵, 30 эпох) и сделал второй вариант режимом по умолчанию. Итог этого подбора виден в разделе 5: перплексия на моей валидации опустилась с 3.58 до 2.15. Урок неоригинален, но выстрадан: на маленьком целевом корпусе ручки дообучения чувствительны — малый шаг и немного эпох.
7.7 Мелочи, тоже оставившие след¶
Три эпизода поменьше, каждый — на полстроки морали.
- Открытые теги стиля. Сначала стиль был зашит одним фиксированным токеном
STYLE_user. Я заменил его открытой системой тегов (мой корпус —H1K0), чтобы будущие стили (например, J-Pop) можно было добавить, не трогая словарь. Урок: оставляй точку расширения там, где заранее знаешь, что данных прибавится. - Гигиена корпуса. Отдельный коммит ушёл на нормализацию переводов строк (CRLF) и исправление ошибок транскрипции. На 68 периодах каждая опечатка весит ощутимо: «мусор на входе — мусор на выходе» здесь не метафора.
- Ловушка с именем
time. Параметр генерации, задающий размер, называетсяtime— и он затеняет стандартный модульtime. Поэтому вapp.pyстоитfrom time import perf_counter, а неimport time: иначе обращение кtime.perf_counter()упало бы на строковом значении размера. Мелкая, но показательная причина предпочесть точечный импорт.
8. Выводы и ограничения¶
Что получилось.
- Конвейер работает целиком:
.chord→ токены → обучение → генерация → обратно.chordи MIDI. - Дообучение снизило перплексию на валидации с 3.58 до 2.15 (−39.8 %): модель заметно лучше предсказывает мои периоды.
- Качественно генерации после дообучения ближе к моему стилю.
Ограничения.
- Корпус небольшой (68 периодов), поэтому оценки имеют высокую дисперсию.
data/holdout/не заполнен — перплексия измерена на валидации, а не на честном тесте.- Модель сводит энгармонию к диезам — это касается записи, а не звучания.
- Обучен по сути один стиль (
H1K0); остальные теги стиля есть в словаре, но на них модель не обучалась.
Дальнейшая работа (за рамками срока).
- Заполнить holdout и пересчитать перплексию на честном тесте.
- Попробовать дообучение на J-Pop корпусе.
- Добавить пост-обработку имён аккордов в бемоли для удобства чтения.