Press "Enter" to skip to content

LSTM机器学习生成音乐

目录

 

LSTM机器学习生成音乐

 

​ 在 网络流量预测入门(二)之LSTM介绍 中对LSTM的原理进行了介绍,在 简单明朗的 RNN 写诗教程 中介绍了如何使用keras构建RNN模型,然后生成五言唐诗。因此,如果对LSTM不了解,建议想去看一看LSTM相关的文章。

 

​ 在这篇博客中,将介绍如何使用keras构建lstm模型,然后自动生成

音乐
。(当然这些音乐只是简单的纯音乐)

 

​ 代码地址: lstm-musichttps://github.com/xiaohuiduan/lstm-music

 

​ 生成的音乐: auto_musichttps://github.com/xiaohuiduan/lstm-music/blob/main/auto_music.mid

 

​ 实际上,使用LSTM生成音乐,与RNN生成诗词并没有什幺很大的不同,原理都是相通的,而在 简单明朗的 RNN 写诗教程 中, 详细 的介绍了代码的执行流程,感兴趣的可以借鉴参考。

 

​ 下面关于音乐(或其组成)的解释,并不是很

严谨
(甚至可能是错误的),不过,在这篇博客的目的并不是为了来介绍音乐的组成以及原理,主要是为了使用LSTM,望勿怪。

 

数据集介绍

 

​ 数据集来自 Classical-Piano-Composer 。部分数据如下所示,一共有92首音乐。

 

 

​ 音乐是mid类型的文件,关于具体说明,参见 How to Generate Music using a LSTM Neural Network in Keras

 

​ 去繁化简,从最简单的角度来说,我们可以理解为 音乐 都是由 音符(note) 组成的就:ok:了。

 

 

​ 比如说,针对于 0fithos.mid 这首音乐,它由以下 音符(note) 组成:

 

 

​ 上图中的每一个字符(如’4′, ‘C5’, ‘E5’),我们可以认为其为一个note。很多个note就组成了一首音乐。

 

​ 因此,在这种情况下,应该定义两个函数,一个函数将mid文件转化成note数组,另一个函数则恰恰相反,将note数组转化成mid文件。

 

将mid转成note数组

 

​ 下面定义 get_notes ,通过这个函数,我们可以将文件夹中所有mid文件变成 一个 名为 all_note 的数组。

 

​ 关于具体怎幺转化,实际上我们没有必要去关心,这个函数也是直接copy 基于深度学习lstm算法生成音乐 的,直接用即可。

 

from music21 import converter, instrument, note, chord, stream
def get_notes(song_path,song_names):
    """获得midi音乐文件中的音符
    :param song_path: [文件的保存地址]
    :type song_path: [str]
    :param song_names: [所有音乐文件的文件名]
    :type song_names: [list]
    :return: [所有符合要求的音符]
    :rtype: [list]
    """
    all_notes = []
    for song_name in song_names:
        stream = converter.parse(song_path+song_name)
        instru = instrument.partitionByInstrument(stream)
        if instru:  # 如果有乐器部分,取第一个乐器部分
            notes = instru.parts[0].recurse()
        else:  #如果没有乐器部分,直接取note
            notes = stream.flat.notes
        for element in notes:
            # 如果是 Note 类型,取音调
            # 如果是 Chord 类型,取音调的序号,存int类型比较容易处理
            if isinstance(element, note.Note):
                all_notes.append(str(element.pitch))
            elif isinstance(element, chord.Chord):
                all_notes.append('.'.join(str(n) for n in element.normalOrder))
    return all_notes

 

将note数组转成mid文件

 

​ 既然可以将mid文件转化成note数组,同理,也可以将note数组转成mid文件(也就是音乐)。定义一个 create_music 函数,同理这个函数也是copy 基于深度学习lstm算法生成音乐 的,同样也不需要关心其如何实现。

 

create_music 函数在使用模型生成音乐的时候会用到(到后面看到的时候别懵逼了哦!!!!)。

 

def create_music(result_data,filename):
    """生成mid音乐,然后进行保存
    :param result_data: [音符列表]
    :type result_data: [list]
    :param filename: [文件名]
    :type filename: [str]
    """
    result_data = [str(data) for data in result_data]
    offset = 0
    output_notes = []
    # 生成 Note(音符)或 Chord(和弦)对象
    for data in result_data:
        if ('.' in data) or data.isdigit():
            notes_in_chord = data.split('.')
            notes = []
            for current_note in notes_in_chord:
                new_note = note.Note(int(current_note))
                new_note.storedInstrument = instrument.Piano()
                notes.append(new_note)
            new_chord = chord.Chord(notes)
            new_chord.offset = offset
            output_notes.append(new_chord)
        else:
            new_note = note.Note(data)
            new_note.offset = offset
            new_note.storedInstrument = instrument.Piano()
            output_notes.append(new_note)
        offset += 1
    # 创建音乐流(Stream)
    midi_stream = stream.Stream(output_notes)
    # 写入 MIDI 文件
    midi_stream.write('midi', fp=filename+'.mid')

 

获取数据集并将其保存

 

​ 通过前面的介绍,调用 get_notes 将使用music21库将 文件夹中所有的mid文件 变成 一个note数组 ,但实际上这个过程是比较慢的,因此可以在第一次的时候将转换后的note数组保存起来,下面定义分别定义保存和读取的函数:

 

def save_data(filename,content):
    """保存音符
    :param filename: [保存的文件名]
    :type filename: [str]
    :param content: [内容]
    :type content: [list]]
    """
    with open(filename,"w") as f:
        for data in content:
            f.write(str(data)+"\n")
def get_data(filename):
    """从文件中获取音符
    :param filename: [文件名]
    :type filename: [str]
    :return: [返回音符]
    :rtype: [list]
    """
    with open(filename) as f:
       all_notes = f.readlines()
    return [ note[:len(note)-1]  for note in all_notes]

 

​ 接下来就是调用以上几个函数:将mid文件转成note数组——>将note数组进行保存。

 

import os
song_path = "./midi_songs/"
song_names = os.listdir(song_path)
# 获取note数组
all_notes = get_notes(song_path,song_names)
# 保存文件
save_data("data.txt",all_notes)

 

将note进行编号

 

​ 面对LSTM网络,当然不可能直接将音符喂给网络,在 简单明朗的 RNN 写诗教程 中详细的介绍了原因,这里就不多赘述。

 

喂的数据是进行one-hot编码后的数据。

 

 

​ 简单点来说,需要对音符进行one-hot编码,因此需要对note进行编号(比如说 “A5” 的编号是 0 , “F5” 的编号是 4 )。

 

当 每一种 音符都有了id(序号)后,就可以很简单的对每一个note都进行one-hot编码了

 

from collections import Counter
# 对出现过的note进行统计
counter = Counter(all_notes)
# 根据出现的次数,进行从大到小的排序
note_count = sorted(counter.items(),key=lambda x : -x[1])
notes,_ = zip(*note_count)
# 产生note到id的映射
note_to_id = {note:id for id,note in enumerate(notes)}

 

note_to_id 的部分数据如下:

 

 

构建数据集

 

截取数据

 

​ 构建数据集的过程原理同样在 简单明朗的 RNN 写诗教程 详细说过,以诗为例,过程如下。

 

 

​ 在上图中,一个 X_Data 的长度是6,这里我们取100。同时我们在取数据的同时将note转换成id。也就是说最后在 X_trainY_train 中数据并不是

note
而是id。

 

X_train = []
Y_train = []
sequence_batch = 100
for i in range(len(all_notes)-sequence_batch):
    X_pre = all_notes[i:i+sequence_batch]
    Y_pre = all_notes[i+sequence_batch]
    X_train.append([note_to_id[note] for note in X_pre])
    Y_train.append(note_to_id[Y_pre])

 

​ 部分结果如下图所示:

 

 

进行one-hot编码

 

​ one-hot编码,这里我们直接使用keras提供工具。 X_one_hotY_one_hot 才是 最终 喂给LSTM的数据。

 

from keras.utils import to_categorical
X_one_hot = to_categorical(X_train)
Y_one_hot = to_categorical(Y_train)

 

 

构建模型

 

模型图如下所示,

 

 

下面是我随便构建的网络模型:

 

import keras
from keras.callbacks import ModelCheckpoint
from keras.models import Input, Model
from keras.layers import  Dropout, Dense,LSTM 
from keras.optimizers import Adam
from keras.utils import plot_model
# X_one_hot.shape[1:] = (100, 308)
input_tensor = Input(shape=X_one_hot.shape[1:])
lstm = LSTM(512,return_sequences=True)(input_tensor)
dropout = Dropout(0.3)(lstm)
lstm = LSTM(256)(dropout)
dropout = Dropout(0.3)(lstm)
# Y_one_hot.shape[-1] = 308
dense = Dense(Y_one_hot.shape[-1], activation='softmax')(dropout)
model = Model(inputs=input_tensor, outputs=dense)
# 画图
# plot_model(model, to_file='model.png', show_shapes=True, expand_nested=True, dpi=500)
optimizer = Adam(lr=0.001)
model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
model.summary()import keras
from keras.callbacks import ModelCheckpoint
from keras.models import Input, Model
from keras.layers import  Dropout, Dense,LSTM 
from keras.optimizers import Adam
from keras.utils import plot_model
# X_one_hot.shape[1:] = (100, 308)
input_tensor = Input(shape=X_one_hot.shape[1:])
lstm = LSTM(512,return_sequences=True)(input_tensor)
dropout = Dropout(0.3)(lstm)
lstm = LSTM(256)(dropout)
dropout = Dropout(0.3)(lstm)
# Y_one_hot.shape[-1] = 308
dense = Dense(Y_one_hot.shape[-1], activation='softmax')(dropout)
model = Model(inputs=input_tensor, outputs=dense)
# 画图
# plot_model(model, to_file='model.png', show_shapes=True, expand_nested=True, dpi=500)
optimizer = Adam(lr=0.001)
model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
model.summary()

 

训练

 

​ 相比较于上一次的RNN写诗,这一次,我们可以将数据集全部放到内存中进行训练,因为此次数据集比较小,可以将其全部放到内存中。不过,还是建议将数据集放到GPU比较好的电脑上面跑(比如说,

白嫖
kaggle,hhh)。

 

filepath = "./{epoch}--weights{loss:.2f}.hdf5"
checkpoint = ModelCheckpoint(
    filepath,
    monitor='loss',
    verbose=0,
    save_best_only=True,
    mode='min'
)
callbacks_list = [checkpoint]
model.fit(X_one_hot, Y_one_hot, epochs=100, batch_size=2048,callbacks=callbacks_list)

 

生成音乐

 

​ 生成音乐的代码没什幺好说的,原理与生成唐诗原理是一样的。生成唐诗的原理如下所示,只不过RNN变成了LSTM,同时数据的长度变成了100罢了。

 

加载数据

 

​ 在前面的操作中,通过 save_data 函数将数据集进行了保存(保存在data.txt文件中),因此,这一次可以直接从 data.txt 文件中读取数据。

 

def get_data(filename):
    """从文件中获取音符
    :param filename: [文件名]
    :type filename: [str]
    :return: [返回音符]
    :rtype: [list]
    """
    with open(filename) as f:
       all_notes = f.readlines()
    return [ note[:len(note)-1]  for note in all_notes]
# 从保存的数据集中获得数据
all_notes = get_data("data.txt")

 

加载模型

 

​ 在 GitHub 中,已经提供了一个训练好的模型供大家使用,不过请尽量保持keras版本一致: 2.4.3

 

# 加载模型
from keras.models import load_model
model = load_model("weights-804-0.01.hdf5")

 

构建id与note的映射

 

​ 通过LSTM,predict出来的肯定不是一个音符,而是一个id,因此,需要构建一个id到note的映射:

 

from collections import Counter
from keras.utils import to_categorical
counter = Counter(all_notes)
note_count = sorted(counter.items(),key=lambda x : -x[1])
notes,_ = zip(*note_count)
# note到id的映射
note_to_id = {note:id for id,note in enumerate(notes)}
# id到note的映射
id_to_note = {id:note for id,note in enumerate(notes)}
# 构建X_train,目的是为了实现随机从X_one_hot选择一个数据,然后进行predict 
X_train = []
sequence_batch = 100
for i in range(len(all_notes)-sequence_batch):
    X_pre = all_notes[i:i+sequence_batch]
    X_train.append([note_to_id[note] for note in X_pre])
X_one_hot = to_categorical(X_train)

 

预测下一个note

 

​ 可以定义一个函数,目的是为了进行predict,函数接受长度为100的note数组,然后返回预测的 id :

 

def predict_next(X_predict):
    """通过前100个音符,预测下一个音符
    :param X_predict: [前100个音符]
    :type X_predict: [list]
    :return: [下一个音符的id]
    :rtype: [int]
    """
    prediction = model.predict(X_predict)
    index = np.argmax(prediction)
    return index

 

源源不断产生note数据

 

​ 一首音乐当然不可能就101个音符(初始给的100个音符,然后通过这100个音符预测下一个音符),因此需要如下图所示,源源不断地进行预测。

 

 

​ 下面定义 generate_notes 函数,目的就是为了产生音符长度为1000的音乐文件。

 

import numpy as np
from music21 import converter, instrument, note, chord, stream
def generate_notes():
    """随机从X_one_hot抽取一个数据(长为100),然后进行predict,最后生成音乐
    :return: [note数组(['D5', '2.6', 'F#5', 'D3', ……])]
    :rtype: [list]
    """
    # 随机从X_one_hot选择一个数据进行predict
    randindex = np.random.randint(0, len(X_one_hot) - 1)
    predict_input = X_one_hot[randindex]
    # music_output里面是一个数组,如['D5', '2.6', 'F#5', 'D3', 'E5', '2.6', 'G5', 'F#5']
    music_output = [id_to_note[id] for id in X_train[randindex]]
    # 产生长度为1000的音符序列
    for note_index in range(1000):
        prediction_input = np.reshape(predict_input, (1,X_one_hot.shape[1],X_one_hot.shape[2]))
        # 预测下一个音符id
        predict_index = predict_next(prediction_input)
        # 将id转换成音符
        music_note = id_to_note[predict_index]
        music_output.append(music_note)
        # X_one_hot.shape[-1] = 308
        one_hot_note = np.zeros(X_one_hot.shape[-1])
        one_hot_note[predict_index] = 1
        one_hot_note = np.reshape(one_hot_note,(1,X_one_hot.shape[-1]))
        # 重新构建LSTM的输入
        predict_input = np.concatenate((predict_input[1:],one_hot_note))
    return music_output

 

​ 调用 generate_notes 函数,便可以产生一定长(1000)序列的note数组。

 

predict_notes = generate_notes()

 

生成音乐

 

​ 通过上一步,产生了一定长序列的note数组了,接在下,调用在前文定义的 将note数组转成mid文件 函数( create_music 函数),便可以将note数组转换成音乐mid文件。

 

create_music(predict_notes,"auto_music")

 

总结

 

​ 以上,便是使用keras构建LSTM生成音乐的全部内容。实际上内容与 简单明朗的 RNN 写诗教程 的过程差不多(可谓是大同小异)。

 

​ 在借助keras API情况下,我们可以很轻松的使用几行代码便可以构建一个lstm模型,但实际上,真正重要的并不是我们如何调用keras的API写代码,而是几行代码后面的原理。

 

参考

 

基于深度学习lstm算法生成音乐

 

How to Generate Music using a LSTM Neural Network in Keras

 

Want to Generate your own Music using Deep Learning? Here’s a Guide to do just that!

 

Classical-Piano-Composer

 

简单明朗的 RNN 写诗教程

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注