Генерируем музыку с помощью LSTM, GRU и Transformer на Keras 3


Сегодня попробуем решить довольно простую, но наглядную задачу: обучить нейросеть генерировать музыку.


В качестве представления музыки будем использовать текстовую нотацию ABC, а в роли генеративных моделей сравним три архитектуры:


  • LSTM;
  • GRU;
  • небольшой Transformer.

Цель статьи — не построить state-of-the-art музыкальную модель уровня современных коммерческих генераторов аудио, а на практическом примере показать, как последовательные нейросети учатся структуре данных и прогнозируют следующий элемент последовательности.


В нашем случае элементом будет обычный символ текста.


Весь пример можно запустить в Jupyter Notebook или Google Colab.


Минимально понадобятся:


  • Python 3.10+;
  • Jupyter Notebook / Google Colab;
  • базовое понимание Python;
  • желательно GPU, хотя небольшой датасет можно обучать и на CPU.

Русский язык не является для меня родным, поэтому содержательные замечания по статье и коду приветствуются.



Почему ABC


Вместо генерации непосредственно WAV-аудио мы сначала будем генерировать символьное описание музыки.


ABC — это текстовый формат музыкальной нотации. Ноты, длительности, тональность, размер и другие элементы композиции записываются обычными символами.


Например:


X:1
T:Example
M:4/4
L:1/8
K:C

[V:S] (BA) !p!G2 |z AGA|(FG) A2|
w: ple-na, Do-mi-nus te-cum,

[V:A] F2 E2|z FEC|(DE) F2|
w: ple-na, Do-mi-nus te-cum,

[V:T] (dc) c2|z ccA|(Ac) c2|

[V:B] (B,,F,) C,2|z F,C,F,|(D,C,) F,2|

Формат удобен для нашего эксперимента по двум причинам.


Во-первых, музыка превращается в обычную последовательность символов.


Во-вторых, результат работы модели можно преобразовать в MIDI, а затем прослушать.


На момент написания статьи актуальным опубликованным стандартом является ABC 2.1, а версия 2.2 находится в статусе draft (Walshaw, 2011).


Наборы ABC-файлов можно найти, например, на:


https://abcnotation.com/
https://abcplus.sourceforge.net/

Для нормального обучения лучше использовать не одну композицию, а коллекцию из десятков, сотен или тысяч произведений в одном стиле.


Установка окружения


Для Python:


!pip install -q tensorflow

Импортируем необходимые библиотеки:


import numpy as np
import tensorflow as tf
import keras

print("TensorFlow:", tf.__version__)
print("Keras:", keras.__version__)

В актуальной ветке TensorFlow 2.21 поддерживаются современные версии Python, а Keras 3 используется как основной высокоуровневый API.


Для преобразования ABC в MIDI установим дополнительные утилиты:


!apt-get update -qq
!apt-get install -y -qq abcmidi timidity

Читаем датасет


Предположим, что у нас есть файл:


music.abc

Считываем его:


with open("music.abc", "r", encoding="utf-8") as f:
    text = f.read()

print("Количество символов:", len(text))
print(text[:500])

Сначала послушаем исходные данные


Если файл содержит корректную ABC-композицию:


!abc2midi "music.abc" -o "music.mid"
!timidity "music.mid" -Ow -o "music.wav"

После этого в Jupyter:


from IPython.display import Audio

Audio("music.wav")

Таким образом мы сначала убеждаемся, что исходный ABC действительно можно интерпретировать как музыку.


Что именно будет изучать нейросеть


Мы будем использовать character-level language model.


То есть нейросеть не знает понятий:


  • нота;
  • аккорд;
  • такт;
  • тональность;
  • мелодия.

Для неё существуют только символы.


Например:


X:1
M:4/4
K:C
CDEF|GABc|

превращается примерно в:


[31, 7, 12, 9, 4, 4, 18, ...]

Задача модели очень похожа на генерацию текста:


имея предыдущие символы, предсказать следующий символ.

Character-level RNN остаётся хорошим учебным примером последовательного моделирования (Karpathy, 2015; TensorFlow, 2026).


Создаём словарь


Сначала найдём все уникальные символы:


vocab = sorted(set(text))

vocab_size = len(vocab)

print("Размер словаря:", vocab_size)
print(vocab)

Важно использовать sorted().


В старых примерах часто встречается:


vocab = set(text)

но порядок элементов set не следует использовать как стабильное соответствие между символом и индексом.


Создадим отображения:


char_to_index = {
    char: index
    for index, char in enumerate(vocab)
}

index_to_char = np.array(vocab)

Теперь преобразуем весь текст в числа:


text_as_int = np.array(
    [char_to_index[c] for c in text],
    dtype=np.int32
)

print(text[:30])
print(text_as_int[:30])

Создание обучающих последовательностей


Допустим, размер контекста составляет 128 символов.


SEQ_LENGTH = 128
STEP = 16
BATCH_SIZE = 64

Каждый пример будет выглядеть примерно так:


INPUT:
X:1
T:Example
M:4/4
...

TARGET:
:1
T:Example
M:4/4
...C

То есть target — это input, сдвинутый на один символ.


Используем современный tf.data pipeline:


dataset = keras.utils.timeseries_dataset_from_array(
    data=text_as_int,
    targets=None,
    sequence_length=SEQ_LENGTH + 1,
    sequence_stride=STEP,
    shuffle=True,
    batch_size=BATCH_SIZE
)

Разделим каждую последовательность:


def split_input_target(batch):
    return batch[:, :-1], batch[:, 1:]

dataset = dataset.map(
    split_input_target,
    num_parallel_calls=tf.data.AUTOTUNE
)

dataset = dataset.prefetch(tf.data.AUTOTUNE)

Теперь:


for x_batch, y_batch in dataset.take(1):
    print(x_batch.shape)
    print(y_batch.shape)

получим приблизительно:


(64, 128)
(64, 128)

На каждом из 128 шагов модель должна предсказать следующий символ.




Модель №1 — LSTM


LSTM была предложена Хохрайтером и Шмидхубером ещё в 1997 году как способ уменьшить проблему исчезающего градиента в рекуррентных сетях (Hochreiter and Schmidhuber, 1997).


Несмотря на возраст архитектуры, LSTM всё ещё удобна как простой baseline для небольших последовательных задач.


В Keras 3 слой LSTM по возможности автоматически использует оптимизированную реализацию для доступного оборудования.


Создадим модель:


EMBED_DIM = 256
RNN_UNITS = 512

def build_lstm_model():
    inputs = keras.Input(
        shape=(SEQ_LENGTH,),
        dtype="int32"
    )

    x = keras.layers.Embedding(
        input_dim=vocab_size,
        output_dim=EMBED_DIM
    )(inputs)

    x = keras.layers.LSTM(
        RNN_UNITS,
        return_sequences=True,
        dropout=0.1
    )(x)

    logits = keras.layers.Dense(
        vocab_size
    )(x)

    model = keras.Model(inputs, logits)

    return model

Создаём модель:


lstm_model = build_lstm_model()
lstm_model.summary()

Обратите внимание: в последнем слое нет softmax.


Мы будем работать непосредственно с logits и использовать:


SparseCategoricalCrossentropy(
    from_logits=True
)

Это немного удобнее и численно стабильнее.


Компиляция:


loss_fn = keras.losses.SparseCategoricalCrossentropy(
    from_logits=True
)

lstm_model.compile(
    optimizer=keras.optimizers.Adam(
        learning_rate=1e-3
    ),
    loss=loss_fn
)

Обучение:


history = lstm_model.fit(
    dataset,
    epochs=10
)

Для первого эксперимента 5–10 эпох обычно достаточно, чтобы увидеть, что модель начинает воспроизводить структуру ABC.


Для хорошего результата важнее не просто увеличить количество эпох, а дать модели достаточно большой и однородный датасет.




Модель №2 — GRU


Можно заменить LSTM на GRU.


GRU была предложена в работе Cho et al. (2014). Она использует более простую систему gating и обычно содержит меньше параметров, чем сопоставимая LSTM.


Для нашей задачи изменение минимально:


def build_gru_model():
    inputs = keras.Input(
        shape=(SEQ_LENGTH,),
        dtype="int32"
    )

    x = keras.layers.Embedding(
        vocab_size,
        EMBED_DIM
    )(inputs)

    x = keras.layers.GRU(
        RNN_UNITS,
        return_sequences=True,
        dropout=0.1
    )(x)

    logits = keras.layers.Dense(
        vocab_size
    )(x)

    return keras.Model(inputs, logits)

Обучение:


gru_model = build_gru_model()

gru_model.compile(
    optimizer=keras.optimizers.Adam(1e-3),
    loss=loss_fn
)

gru_model.fit(
    dataset,
    epochs=10
)

Для подобных небольших задач GRU стоит попробовать хотя бы потому, что она часто обучается быстрее, чем LSTM.




Модель №3 — Transformer


В 2026 году обсуждать генерацию последовательностей только через LSTM было бы несколько странно.


После появления архитектуры Transformer (Vaswani et al., 2017) attention-модели стали основой большинства современных языковых моделей.


Для нашей небольшой задачи гигантский Transformer не нужен.


Сделаем маленький causal Transformer.


Embedding + positional embedding


Transformer сам по себе не знает порядок элементов, поэтому добавим информацию о позиции символа.


@keras.saving.register_keras_serializable(
    package="Music"
)
class TokenAndPositionEmbedding(keras.layers.Layer):

    def __init__(
        self,
        maxlen,
        vocab_size,
        embed_dim,
        **kwargs
    ):
        super().__init__(**kwargs)

        self.maxlen = maxlen
        self.vocab_size = vocab_size
        self.embed_dim = embed_dim

        self.token_emb = keras.layers.Embedding(
            input_dim=vocab_size,
            output_dim=embed_dim
        )

        self.pos_emb = keras.layers.Embedding(
            input_dim=maxlen,
            output_dim=embed_dim
        )

    def call(self, x):
        length = tf.shape(x)[-1]

        positions = tf.range(
            start=0,
            limit=length,
            delta=1
        )

        return (
            self.token_emb(x)
            + self.pos_emb(positions)
        )

    def get_config(self):
        config = super().get_config()

        config.update({
            "maxlen": self.maxlen,
            "vocab_size": self.vocab_size,
            "embed_dim": self.embed_dim
        })

        return config

Transformer block


@keras.saving.register_keras_serializable(
    package="Music"
)
class TransformerBlock(keras.layers.Layer):

    def __init__(
        self,
        embed_dim,
        num_heads,
        ff_dim,
        dropout=0.1,
        **kwargs
    ):
        super().__init__(**kwargs)

        self.embed_dim = embed_dim
        self.num_heads = num_heads
        self.ff_dim = ff_dim
        self.dropout_rate = dropout

        self.att = keras.layers.MultiHeadAttention(
            num_heads=num_heads,
            key_dim=embed_dim // num_heads,
            dropout=dropout
        )

        self.ffn = keras.Sequential([
            keras.layers.Dense(
                ff_dim,
                activation="gelu"
            ),
            keras.layers.Dense(embed_dim)
        ])

        self.norm1 = keras.layers.LayerNormalization()
        self.norm2 = keras.layers.LayerNormalization()

        self.dropout1 = keras.layers.Dropout(dropout)
        self.dropout2 = keras.layers.Dropout(dropout)

    def call(self, x, training=False):

        attention = self.att(
            x,
            x,
            use_causal_mask=True,
            training=training
        )

        attention = self.dropout1(
            attention,
            training=training
        )

        x = self.norm1(x + attention)

        ffn_output = self.ffn(x)

        ffn_output = self.dropout2(
            ffn_output,
            training=training
        )

        return self.norm2(
            x + ffn_output
        )

    def get_config(self):
        config = super().get_config()

        config.update({
            "embed_dim": self.embed_dim,
            "num_heads": self.num_heads,
            "ff_dim": self.ff_dim,
            "dropout": self.dropout_rate
        })

        return config

Теперь сама модель:


def build_transformer_model():

    EMBED = 256
    HEADS = 4
    FF_DIM = 512

    inputs = keras.Input(
        shape=(SEQ_LENGTH,),
        dtype="int32"
    )

    x = TokenAndPositionEmbedding(
        SEQ_LENGTH,
        vocab_size,
        EMBED
    )(inputs)

    x = TransformerBlock(
        embed_dim=EMBED,
        num_heads=HEADS,
        ff_dim=FF_DIM
    )(x)

    x = TransformerBlock(
        embed_dim=EMBED,
        num_heads=HEADS,
        ff_dim=FF_DIM
    )(x)

    logits = keras.layers.Dense(
        vocab_size
    )(x)

    return keras.Model(
        inputs,
        logits
    )

Компиляция:


transformer_model = build_transformer_model()

transformer_model.compile(
    optimizer=keras.optimizers.Adam(
        learning_rate=3e-4
    ),
    loss=loss_fn
)

transformer_model.summary()

Обучение:


transformer_model.fit(
    dataset,
    epochs=10
)

MultiHeadAttention в Keras 3 поддерживает causal attention, поэтому каждый символ видит только предыдущую часть последовательности.


Это принципиально важно для авторегрессионной генерации.




Какую модель выбрать


Для этого эксперимента я бы сравнивал все три.


Примерная картина:


Модель Плюсы Минусы
LSTM простая, понятная, хороший baseline медленнее обрабатывает длинные зависимости
GRU проще LSTM, меньше параметров те же фундаментальные ограничения RNN
Transformer attention, хорошо масштабируется сложнее, требует больше данных и памяти

На маленьком ABC-датасете Transformer совсем не обязательно даст лучший результат.


Это важный момент.


Более современная архитектура не компенсирует плохие или слишком маленькие данные.




Генерация музыки


Теперь самая интересная часть.


Старая версия кода выбирала следующий символ примерно так:


np.argmax(
    np.random.multinomial(
        1,
        predictions
    )
)

Работать это может, но лучше контролировать генерацию через:


  • temperature;
  • top-k sampling.

Temperature регулирует случайность.


При низкой температуре:


temperature = 0.3

модель выбирает более ожидаемые символы.


При высокой:


temperature = 1.2

результат становится более случайным.


Создадим функцию:


def sample_from_logits(
    logits,
    temperature=0.8,
    top_k=20
):

    temperature = max(
        temperature,
        1e-5
    )

    logits = logits / temperature

    if top_k is not None:

        k = min(
            top_k,
            vocab_size
        )

        values, indices = tf.math.top_k(
            logits,
            k=k
        )

        sampled_index = tf.random.categorical(
            values[None, :],
            num_samples=1
        )[0, 0]

        return int(
            indices[sampled_index]
        )

    sampled_index = tf.random.categorical(
        logits[None, :],
        num_samples=1
    )[0, 0]

    return int(sampled_index)

Теперь генератор:


def generate_text(
    model,
    start_string,
    generation_length=1000,
    temperature=0.8,
    top_k=20
):

    prompt_ids = [
        char_to_index[c]
        for c in start_string
        if c in char_to_index
    ]

    if len(prompt_ids) == 0:
        raise ValueError(
            "Prompt не содержит известных символов"
        )

    generated = []

    pad_id = char_to_index.get(
        "\n",
        0
    )

    window = np.full(
        (SEQ_LENGTH,),
        pad_id,
        dtype=np.int32
    )

    prompt_ids = prompt_ids[
        -SEQ_LENGTH:
    ]

    window[
        -len(prompt_ids):
    ] = prompt_ids

    for _ in range(
        generation_length
    ):

        x = window[None, :]

        logits = model(
            x,
            training=False
        )[0, -1]

        next_id = sample_from_logits(
            logits,
            temperature=temperature,
            top_k=top_k
        )

        generated.append(
            index_to_char[next_id]
        )

        window[:-1] = window[1:]
        window[-1] = next_id

    return (
        start_string
        + "".join(generated)
    )

В качестве начала лучше использовать не просто:


X:

а корректный ABC-header:


prompt = """X:1
T:Neural composition
M:4/4
L:1/8
K:C
"""

Запускаем:


new_song = generate_text(
    lstm_model,
    start_string=prompt,
    generation_length=1500,
    temperature=0.8,
    top_k=20
)

print(new_song)

Попробуйте затем:


temperature=0.4

и:


temperature=1.2

Разница обычно хорошо заметна.




Сохраняем результат


with open(
    "new_song.abc",
    "w",
    encoding="utf-8"
) as f:

    f.write(new_song)

Проверяем, получилось ли у модели создать синтаксически допустимый ABC:


!abc2midi "new_song.abc" -o "new_song.mid"

Если парсер выдаёт ошибку — это тоже результат эксперимента.


Нейросеть не знает грамматику ABC напрямую.


Она лишь статистически изучает её по примерам.


Если MIDI успешно создан:


!timidity "new_song.mid" -Ow -o "new_song.wav"

Слушаем:


Audio("new_song.wav")



Сохранение модели


В Keras 3 рекомендуется сохранять полноценную модель в формате .keras.


Например:


lstm_model.save(
    "abc_lstm.keras"
)

Загрузка:


model = keras.models.load_model(
    "abc_lstm.keras"
)

Файл содержит:


  • архитектуру;
  • веса;
  • состояние оптимизатора;
  • конфигурацию обучения.

Для Transformer с зарегистрированными через register_keras_serializable слоями аналогично:


transformer_model.save(
    "abc_transformer.keras"
)



Что можно улучшить


Наш пример намеренно простой.


Если развивать проект дальше, я бы начал со следующих экспериментов.


1. Увеличить датасет


Одна композиция практически гарантирует переобучение.


Лучше использовать сотни или тысячи ABC-композиций одного жанра.


Например:


Irish folk
Baroque
Jazz
Choral music

Отдельная модель для относительно однородного набора обычно интереснее, чем смесь совершенно разных музыкальных стилей.


2. Генерировать не символы, а музыкальные токены


Character-level representation очень простое, но не обязательно оптимальное.


Можно отдельно токенизировать:


NOTE_C4
NOTE_E4
DURATION_1_4
BAR
REST
CHORD

Тогда модель начинает работать уже не с отдельными буквами, а с музыкальными событиями.


3. Использовать MIDI-event representation


Следующий шаг — отказаться от ABC и представить MIDI как последовательность событий:


NOTE_ON
NOTE_OFF
TIME_SHIFT
VELOCITY

Такой подход ближе к современной symbolic music generation.


4. Добавить conditioning


Например, передавать модели:


STYLE=BAROQUE
KEY=C_MINOR
TEMPO=120

и генерировать музыку с заданными параметрами.


5. Увеличить Transformer


Для достаточно большого датасета можно использовать:


  • 4–8 Transformer blocks;
  • embedding 256–512;
  • context 512–2048 токенов;
  • learning-rate warmup;
  • AdamW;
  • gradient clipping.

Но для небольшого учебного датасета увеличение модели часто просто ускорит переобучение.




Что изменилось по сравнению со старыми примерами LSTM


Подобные tutorials были очень популярны несколько лет назад.


В версии на 2026 год я бы выделил несколько изменений.


Во-первых, вместо формирования огромного numpy-массива последовательностей удобнее использовать tf.data.


Во-вторых, лучше возвращать из модели logits и использовать:


SparseCategoricalCrossentropy(
    from_logits=True
)

В-третьих, для генерации имеет смысл использовать temperature и top-k sampling.


В-четвёртых, LSTM сегодня полезно рассматривать именно как baseline и сравнивать хотя бы с GRU и Transformer.


Наконец, модель и её конфигурацию теперь удобно сохранять в нативном формате Keras:


.keras



Итоги


Мы построили простую систему генерации музыки:


ABC
 ↓
токенизация символов
 ↓
LSTM / GRU / Transformer
 ↓
генерация новой ABC-последовательности
 ↓
abc2midi
 ↓
MIDI
 ↓
WAV

Самое интересное здесь не столько качество музыки, сколько сам факт того, что модель получает только длинную последовательность символов и самостоятельно начинает обнаруживать в ней структуру:


  • заголовки;
  • такты;
  • ноты;
  • длительности;
  • повторения;
  • отдельные синтаксические конструкции ABC.

LSTM отлично подходит для демонстрации этой идеи.


GRU даёт более компактный рекуррентный baseline.


Transformer показывает, как ту же задачу можно решать с использованием attention.


При этом важно понимать: современные системы генерации музыки ушли значительно дальше и могут использовать event-based Transformer-модели, latent representations, diffusion и генерацию непосредственно в аудиопространстве.


Но именно поэтому такой небольшой эксперимент остаётся полезным: здесь весь pipeline можно разобрать буквально построчно.


Если попробуете обучить модель на своём ABC-датасете — интересно будет сравнить результаты LSTM, GRU и Transformer.


Список литературы


Briot, J.-P., Hadjeres, G. and Pachet, F.-D. (2020) Deep Learning Techniques for Music Generation. Cham: Springer.


Cho, K., van Merriënboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H. and Bengio, Y. (2014) ‘Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation’, Proceedings of EMNLP 2014, pp. 1724–1734. Available at: https://arxiv.org/abs/1406.1078 .


Hochreiter, S. and Schmidhuber, J. (1997) ‘Long Short-Term Memory’, Neural Computation, 9(8), pp. 1735–1780. doi:10.1162/neco.1997.9.8.1735.


Karpathy, A. (2015) ‘The Unreasonable Effectiveness of Recurrent Neural Networks’. Available at: https://karpathy.github.io/2015/05/21/rnn-effectiveness/ (.


Keras Team (2026a) LSTM layer — Keras 3 API documentation. Available at: https://keras.io/api/layers/recurrent_layers/lstm/ .


Keras Team (2026b) MultiHeadAttention layer — Keras 3 API documentation. Available at: https://keras.io/api/layers/attention_layers/multi_head_attention/ .


Keras Team (2026c) Whole model saving and loading. Available at: https://keras.io/api/models/model_saving_apis/model_saving_and_loading/


TensorFlow (2026a) Text generation with an RNN. Available at: https://www.tensorflow.org/text/tutorials/text_generation .


TensorFlow (2026b) Install TensorFlow with pip. Available at: https://www.tensorflow.org/install/pip (Accessed: 23 September 2026).


Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A.N., Kaiser, Ł. and Polosukhin, I. (2017) ‘Attention Is All You Need’, Advances in Neural Information Processing Systems, 30.


Walshaw, C. (2011) The ABC Music Standard 2.1. Available at: https://abcnotation.com/wiki/abc:standard:v2.1 .


ABC Notation (2026) The ABC Standard. Available at: https://abcnotation.com/wiki/abc:standard