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)组成的就🆗了。

​ 比如说,针对于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写代码,而是几行代码后面的原理。

参考

posted @ 2021-02-04 21:48  渣渣辉啊  阅读(3393)  评论(0编辑  收藏  举报