开发之前我们了解midi格式、midi格式与py相关的库和一些操作等。
最常见的音乐格式应该是mp3格式,而midi格式很多时候依靠电脑本身的软件也是打不开的,所以我们先编写一个midi文件转mp3格式的函数
def convert_midi_to_mp3():
#被转换的midi文件名字,默认output
input_file = 'output.mid'
#转后的mp3文件名
output_file = 'output.mp3'
#判断时候存在该文件
if not os.path.exists(input_file):
raise Exception("MIDI 文件 {} 不在此目录下,请确保此文件被正确生成".format(input_file))
print('将 {} 转换为 MP3'.format(input_file))
command = 'timidity {} -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 64k {}'.format(input_file, output_file)
return_code = subprocess.call(command, shell=True)
#输出转换信息
if return_code != 0:
print('转换时出错,请手动转换或下载midi播放器')
else:
print('转换完毕. 生成的文件是 {}'.format(output_file))
再编写读取所以音乐文件音符和和弦的音调的函数,因为和弦也是由音符组成,所有碰到和弦是转化为音符处理
def get_notes():
#读取所有midi文件
if not os.path.exists("music_midi"):
raise Exception("包含所有 MIDI 文件的 music_midi 文件夹不在此目录下,请添加")
notes = []
# glob : 匹配所有符合条件的文件,并以 List 的形式返回
#glob模块是最简单的模块之一,内容非常少。用它可以查找符合特定规则的文件路径名。
for midi_file in glob.glob("music_midi/*.mid"):
#music21.converter.parse方法
stream = converter.parse(midi_file)
# nstrument.partitionByInstrument(stream) 获取所有乐器部分。
parts = instrument.partitionByInstrument(stream)
if parts: # 如果有乐器部分,取第一个乐器部分,否则难度太大了
notes_to_parse = parts.parts[0].recurse()
else:
notes_to_parse = stream.flat.notes
#取音调,若是和弦,则转成音符
for element in notes_to_parse:
# 如果是 Note 类型,那么取它的音调
if isinstance(element, note.Note):
notes.append(str(element.pitch))
# 如果是 Chord 类型,那么取它各个音调的序号
elif isinstance(element, chord.Chord):
notes.append('.'.join(str(n) for n in element.normalOrder))
#保存所取的音调
# 如果 data 目录不存在,创建此目录
if not os.path.exists("data"):
os.mkdir("data")
# 将数据写入 data 目录下的 notes 文件
with open('data/notes', 'wb') as filepath:
pickle.dump(notes, filepath)
return notes
然后再来构造神经网络:
结构为:LSTM(512)->Dropout(30%)->LSTM(512)->Dropout(30%)->LSTM(512)->Dense(256)->Dropout(30%)->Dense(音调数)->Softmax(算概率)
#RNN-LSTM 循环神经网络
import tensorflow as tf
#输入、音符的个数、weights_file用于区分训练和生成
def network_model(inputs, num_pitch, weights_file=None):
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.LSTM(
512, # LSTM 层神经元的数目是 512,也是 LSTM 层输出的维度
input_shape=(inputs.shape[1], inputs.shape[2]), # 输入的形状,对第一个 LSTM 层必须设置
# return_sequences:控制返回类型
# - True:返回所有的输出序列
# - False:返回输出序列的最后一个输出
# 在堆叠 LSTM 层时必须设置,最后一层 LSTM 可以不用设置
return_sequences=True # 返回所有的输出序列(Sequences)
))
model.add(tf.keras.layers.Dropout(0.3)) # 丢弃 30% 神经元,防止过拟合
model.add(tf.keras.layers.LSTM(512, return_sequences=True))
model.add(tf.keras.layers.Dropout(0.3))
model.add(tf.keras.layers.LSTM(512)) # return_sequences 是默认的 False,只返回输出序列的最后一个
model.add(tf.keras.layers.Dense(256)) # 256 个神经元的全连接层
model.add(tf.keras.layers.Dropout(0.3))
model.add(tf.keras.layers.Dense(num_pitch)) # 输出的数目等于所有不重复的音调的数目
model.add(tf.keras.layers.Activation('softmax')) # Softmax 激活函数算概率
# 交叉熵计算误差,使用对 循环神经网络来说比较优秀的 RMSProp 优化器
# 计算误差(先用 Softmax 计算百分比概率,再用 Cross entropy(交叉熵)来计算百分比概率和对应的独热码之间的误差)
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')
if weights_file is not None: # 如果是 生成 音乐时
# 从 HDF5 文件中加载所有神经网络层的参数(Weights)
model.load_weights(weights_file)
return model
我们训练的数据是音乐的序列文件,而输出则是根据n个音符预测出的一个音符,所有我们这里设100个音符预测1个音符
#为神经网络准备好供训练的序列
def prepare_sequences(notes, num_pitch):
sequence_length = 100 # 序列长度
# 得到所有音调的名字
pitch_names = sorted(set(item for item in notes))
# 创建一个字典,用于映射 音调 和 整数
pitch_to_int = dict((pitch, num) for num, pitch in enumerate(pitch_names))
# 创建神经网络的输入序列和输出序列
network_input = []
network_output = []
for i in range(0, len(notes) - sequence_length, 1):
#每次取sequence_length个音符
sequence_in = notes[i: i + sequence_length]
#sequence_length个音符推出的一个音符
sequence_out = notes[i + sequence_length]
#更新序列
network_input.append([pitch_to_int[char] for char in sequence_in])
network_output.append(pitch_to_int[sequence_out])
n_patterns = len(network_input)
# 将输入的形状转换成神经网络模型可以接受的
network_input = np.reshape(network_input, (n_patterns, sequence_length, 1))
# 将 输入 标准化 / 归一化
# 归一话可以让之后的优化器(optimizer)更快更好地找到误差最小值
network_input = network_input / float(num_pitch)
# 将期望输出转换成 {0, 1} 组成的布尔矩阵,为了配合 categorical_crossentropy 误差算法使用
network_output = tf.keras.utils.to_categorical(network_output)
return network_input, network_output
然后我们开始训练模型,训练过程中使用Checkpoint保存训练的参数,目的是:防止训练未完成时中断我们也能得到当前的训练结果和生成时读取该模型参数文件
# 训练神经网络
def train():
notes = get_notes()
# 得到所有不重复(因为用了 set)的音调数目
num_pitch = len(set(notes))
network_input, network_output = prepare_sequences(notes, num_pitch)
model = network_model(network_input, num_pitch)
#官方定义的格式
filepath = "weights.{epoch:02d}-{loss:.4f}.hdf5"
# 用 Checkpoint(检查点)文件在每一个 Epoch 结束时保存模型的参数(Weights)
# 不怕训练过程中丢失模型参数。可以在我们对 Loss(损失)满意了的时候随时停止训练
checkpoint = tf.keras.callbacks.ModelCheckpoint(
filepath, # 保存的文件路径
monitor='loss', # 监控的对象是 损失(loss)
verbose=0,
save_best_only=True, # 不替换最近的数值最佳的监控对象的文件
mode='min' # 取损失最小的
)
callbacks_list = [checkpoint]
# 用 fit 方法来训练模型
model.fit(network_input, network_output, epochs=100, batch_size=64, callbacks=callbacks_list)
我们将midi文件转化为数字序列后进行训练,得出的结果也是数字序列,我们要写个数据->midi的一个函数(具体就不说了,我也学的一知半解的)
def create_music(prediction):
"""
用神经网络'预测'的音乐数据来生成 MIDI 文件,再转成 MP3 文件
"""
offset = 0 # 偏移
output_notes = []
# 生成 Note(音符)或 Chord(和弦)对象
for data in prediction:
# 是 Chord。格式例如: 4.15.7
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() # 乐器用钢琴 (piano)
notes.append(new_note)
new_chord = chord.Chord(notes)
new_chord.offset = offset
output_notes.append(new_chord)
# 是 Note
else:
new_note = note.Note(data)
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
output_notes.append(new_note)
# 每次迭代都将偏移增加,这样才不会交叠覆盖
offset += 0.5
# 创建音乐流(Stream)
midi_stream = stream.Stream(output_notes)
# 写入 MIDI 文件
midi_stream.write('midi', fp='output.mid')
# 将生成的 MIDI 文件转换成 MP3
convert_midi_to_mp3()
最后编写生成函数,首先写一个,给一段初始序列然后调用模型的函数
#基于一序列音符,用神经网络来生成新的音符
def produce_notes(model, network_input, pitch_names, num_pitch):
# 从输入里随机选择一个序列,作为生成的音乐的起始点
start = np.random.randint(0, len(network_input) - 1)
# 创建一个字典,用于映射 整数 和 音调
int_to_pitch = dict((num, pitch) for num, pitch in enumerate(pitch_names))
pattern = network_input[start]
# 神经网络实际生成的音调
prediction_output = []
# 生成 700 个 音符/音调
for note_index in range(700):
prediction_input = np.reshape(pattern, (1, len(pattern), 1))
# 输入 归一化
prediction_input = prediction_input / float(num_pitch)
# 用载入了训练所得最佳参数文件的神经网络来 预测/生成 新的音调
prediction = model.predict(prediction_input, verbose=0)
# argmax 取最大的那个维度(类似 One-Hot 独热码)
index = np.argmax(prediction)
# 将 整数 转成 音调
result = int_to_pitch[index]
prediction_output.append(result)
# 往后移动1个单位
pattern.append(index)
pattern = pattern[1:len(pattern)]
print(note_index)
return prediction_output
再写一个处理初试音符序列和调用上述函数的函数
# 加载用于训练神经网络的音乐数据
def produce():
with open('data/notes', 'rb') as filepath:
notes = pickle.load(filepath)
# 得到所有音调的名字
pitch_names = sorted(set(item for item in notes))
# 得到所有不重复(因为用了set)的音调数目
num_pitch = len(set(notes))
network_input, normalized_input = prepare_sequences(notes, pitch_names, num_pitch)
# 载入之前训练时最好的参数文件(最好用 loss 最小 的那一个参数文件,
# 记得要把它的名字改成 best-weights.hdf5 ),来生成神经网络模型
model = network_model(normalized_input, num_pitch, "/Users/ren/Desktop/best-weights.hdf5")
# 用神经网络来生成音乐数据
prediction = produce_notes(model, network_input, pitch_names, num_pitch)
# 用预测的音乐数据生成 MIDI 文件,再转换成 MP3
create_music(prediction)
最后调用即可
if __name__ == '__main__':
produce()
(注意:best-weights.hdf5是从参数文件中选择的训练最好的参数文件,并手动命名的)
(项目及训练数据后续上传,或者评论留邮箱发送)
(以上内容为学习课程所学并整理)