LSTM机器学习生成音乐
LSTM机器学习生成音乐
在网络流量预测入门(二)之LSTM介绍中对LSTM的原理进行了介绍,在简单明朗的 RNN 写诗教程中介绍了如何使用keras构建RNN模型,然后生成五言唐诗。因此,如果对LSTM不了解,建议想去看一看LSTM相关的文章。
在这篇博客中,将介绍如何使用keras构建lstm模型,然后自动生成音乐。(当然这些音乐只是简单的纯音乐)
代码地址:lstm-music:https://github.com/xiaohuiduan/lstm-music
生成的音乐:auto_music:https://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_train
和Y_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_hot
和Y_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写代码,而是几行代码后面的原理。