Keras深度学习实战(40)——音频生成
0. 前言
我们已经在《文本生成模型》一节中学习了如何利用深度神经网络生成文本,除了文本数据外,音频是另一类重要的时序序列,音频生成对于音频数据增广、艺术创作等领域都有着重要作用。在本节中,我们将学习如何使用深度神经网络生成音频。
1. 模型与数据集分析
1.1 数据集分析
本节中所用数据集为 MIDI
文件,MIDI
文件通常包含有关音频文件的音符与和弦信息,其中音符对象包含有关音符的音高、八度和偏移信息,和弦对象包含一组同时演奏的音符。为了简单起见,我们仅使用一个 MIDI
文件进行训练以快速得到音频生成模型,相关数据集可以在 gitcode 链接中下载。
1.2 模型分析
在实际构建模型之前,我们首先熟悉用于构建音乐生成器的策略策略:
- 提取音频文件中存在的音符
- 为每个音符分配一个唯一的
ID
- 使用滑动窗口,每次记录
100
个音符序列,然后将第101
个音符作为输出 - 构建用于生成音符的长短时记忆网络 (
Long Short Term Memory
,LSTM
) 模型,并进行拟合
2. 音频生成模型
接下来,我们使用 Keras
实现上述音频生成策略。
2.1 数据集加载与预处理
(1) 导入所需的库和数据集,并读取音频文件内容:
import numpy as np
from music21 import converter, instrument, note, chord, stream
from keras.utils import np_utils
from keras.layers import LSTM, Dropout, Dense, Activation
from keras.models import Sequential
from matplotlib import pyplot as plt
fname = 'Pokemon_Fire_Red_Route.mid'
midi = converter.parse(fname)
(2) 定义函数,用于读取乐谱流并从中提取音符,如果音频文件中存在休止符,则还需要提取休止符:
def parse_with_silence(midi=midi):
notes = []
notes_to_parse = None
parts = instrument.partitionByInstrument(midi)
if parts: # 文件包含乐器部分
notes_to_parse = parts.parts[1].recurse()
else: # 文件中包含平滑结构的音符
notes_to_parse = midi.flat.notes
for ix, element in enumerate(notes_to_parse):
if isinstance(element, note.Note):
_note = str(element.pitch)
notes.append(_note)
elif isinstance(element, chord.Chord):
_note = '.'.join(str(n) for n in element.normalOrder)
notes.append(_note)
elif isinstance(element, note.Rest):
_note = '#' + str(element.seconds)
notes.append(_note)
return notes
在以上代码中,通过循环遍历元素来获取音符,并根据元素是音符,和弦还是休止符来提取相应的音符,将其追加至 notes
列表中。
使用 parse_with_silence
函数从输入音频文件流中提取音符:
notes = parse_with_silence(midi)
print(notes)
数据样本的音符示例输出如下,需要注意的是,以 #
开头的值表示休止符,#
旁边的数字表示休止符的持续时间:
['#2.0', '#2.0', '#1.75', 'E-5', 'F5', 'E-3', '#2.0', 'G5', ...]
(3) 通过创建音符 ID
及其对应名称的字典来创建输入和输出数据集:
# 获取音符中的所有唯一值
pitchnames = sorted(set(item for item in notes))
# 创建字典以将音高映射到整数
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))
network_input = []
network_output = []
(4) 创建输入和输出数组序列,使用滑动窗口,每次将 100
个音符序列作为输入,并将第 101
个时间戳中的音符作为输出,此外,还需要将音符转换为相应的 ID
:
sequence_length = 100
for i in range(0, len(notes) - sequence_length, 1):
sequence_in = notes[i:i + sequence_length]
sequence_out = notes[i + sequence_length]
network_input.append([note_to_int[char] for char in sequence_in])
network_output.append(note_to_int[sequence_out])
(5) 接下来,整形输入数据的形状,以便可以将其输入到 LSTM
网络层中,LSTM
层接受的输入形状为 (批大小, 时间戳数, 每个时间戳的特征数)
,同时,对输入进行归一化,并将输出转换为独热编码矢量。
n_patterns = len(network_input)
# 将输入整形能够输入到 LSTM 层的形状
network_input = np.reshape(network_input, (n_patterns, sequence_length, 1))
# 将输入标准化
network_input = network_input / np.max(network_input)
network_output = np_utils.to_categorical(network_output)
2.2 模型构建与训练
(1) 构建并编译音频生成模型:
model = Sequential()
model.add(LSTM(
256,
input_shape=(network_input.shape[1], network_input.shape[2]),
return_sequences=True
))
model.add(Dropout(0.3))
model.add(LSTM(512, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256))
model.add(Dense(256))
model.add(Dropout(0.3))
model.add(Dense(network_output.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
model.summary()
print(network_input.shape, network_output.shape)
# (41303, 100, 1) (41303, 56)
模型的简要架构信息输入如下:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 100, 256) 264192
_________________________________________________________________
dropout (Dropout) (None, 100, 256) 0
_________________________________________________________________
lstm_1 (LSTM) (None, 100, 512) 1574912
_________________________________________________________________
dropout_1 (Dropout) (None, 100, 512) 0
_________________________________________________________________
lstm_2 (LSTM) (None, 256) 787456
_________________________________________________________________
dense (Dense) (None, 256) 65792
_________________________________________________________________
dropout_2 (Dropout) (None, 256) 0
_________________________________________________________________
dense_1 (Dense) (None, 56) 14392
_________________________________________________________________
activation (Activation) (None, 56) 0
=================================================================
Total params: 2,706,744
Trainable params: 2,706,744
Non-trainable params: 0
_________________________________________________________________
(2) 拟合模型,并绘制模型训练过程中损失值的变化情况:
history = model.fit(network_input, network_output, epochs=100, batch_size=32, verbose = 1)
history_dict = history.history
loss_values = history_dict['loss']
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, loss_values, marker='x', label='Traing loss')
plt.title('Training loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
(3) 接下来,我们使用训练完成的模型生成音频:
start = np.random.randint(0, len(network_input)-1)
int_to_note = dict((number, note) for number, note in enumerate(pitchnames))
pattern = network_input[start].tolist()
prediction_output = []
在以上代码中,选择了一个随机的音频位置,并从该位置开始采样一个序列,该序列将用作生成将来时间戳音符的种子。
生成预测的过程,与模型训练过程相同,一次输入 100
个音符序列,生成下一个预测,将其附加到输入序列中,然后通过获取最近 100
个音符的最新序列生成下一个预测:
# 生成 500 个音符
for note_index in range(500):
prediction_input = np.reshape(pattern, (1, len(pattern), 1))
prediction_input = prediction_input
prediction = model.predict(prediction_input, verbose=0)
index = np.argmax(prediction)
result = int_to_note[index]
prediction_output.append(result)
pattern.append([index/48])
pattern = pattern[1:len(pattern)]
需要注意的是,在以上代码中,我们将索引(即模型的预测输出)除以最大索引,就像我们在构建模型输入时所进行的归一化处理(除以 np.max(network_input)
)一样。
音乐生成与文本生成任务略有不同,在文本生成模型中,我们根据输入单词 ID
之上生成嵌入,但在音乐生成中我们并没有使用嵌入,这是由于输入中的唯一值较少,因此该模型在没有嵌入这种情况下仍然可以生成较为令人满意的音乐。
(4) 然后,根据模型生成的值创建音符,同时,我们将每个音符偏移 0.5
秒,以便在生成输出时音符之间不会彼此重叠:
offset = 0
output_notes = []
# 根据模型生成的值创建音符和和弦对象
for pattern in prediction_output:
# pattern 为和弦
if (('.' in pattern) or pattern.isdigit()) and pattern[0] != '#':
notes_in_chord = pattern.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)
# pattern 为音符
elif pattern[0] != '#':
new_note = note.Note(pattern)
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
output_notes.append(new_note)
else:
new_note = note.Rest()
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
new_note.quarterLength = float(pattern[1:])
output_notes.append(new_note)
# 增加每次迭代的偏移量,以便音符并不会重叠
offset += 0.5
最后,将生成的预测写入音乐流文件:
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='test_output.mid')
小结
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
系列链接
Keras深度学习实战(1)——神经网络基础与模型训练过程详解
Keras深度学习实战(2)——使用Keras构建神经网络
Keras深度学习实战(3)——神经网络性能优化技术
Keras深度学习实战(4)——深度学习中常用激活函数和损失函数详解
Keras深度学习实战(5)——批归一化详解
Keras深度学习实战(6)——深度学习过拟合问题及解决方法
Keras深度学习实战(7)——卷积神经网络详解与实现
Keras深度学习实战(8)——使用数据增强提高神经网络性能
Keras深度学习实战(9)——卷积神经网络的局限性
Keras深度学习实战(10)——迁移学习详解
Keras深度学习实战(11)——可视化神经网络中间层输出
Keras深度学习实战(12)——面部特征点检测
Keras深度学习实战(13)——目标检测基础详解
Keras深度学习实战(14)——从零开始实现R-CNN目标检测
Keras深度学习实战(15)——从零开始实现YOLO目标检测
Keras深度学习实战(16)——自编码器详解
Keras深度学习实战(17)——使用U-Net架构进行图像分割
Keras深度学习实战(18)——语义分割详解
Keras深度学习实战(19)——使用对抗攻击生成可欺骗神经网络的图像
Keras深度学习实战(20)——DeepDream模型详解
Keras深度学习实战(21)——神经风格迁移详解
Keras深度学习实战(22)——生成对抗网络详解与实现
Keras深度学习实战(23)——DCGAN详解与实现
Keras深度学习实战(24)——从零开始构建单词向量
Keras深度学习实战(25)——使用skip-gram和CBOW模型构建单词向量
Keras深度学习实战(26)——文档向量详解
Keras深度学习实战(27)——循环神经详解与实现
Keras深度学习实战(28)——利用单词向量构建情感分析模型
Keras深度学习实战(29)——长短时记忆网络详解与实现
Keras深度学习实战(30)——使用文本生成模型进行文学创作
Keras深度学习实战(31)——构建电影推荐系统
Keras深度学习实战(32)——基于LSTM预测股价
Keras深度学习实战(33)——基于LSTM的序列预测模型
Keras深度学习实战(34)——构建聊天机器人
Keras深度学习实战(35)——构建机器翻译模型
Keras深度学习实战(36)——基于编码器-解码器的机器翻译模型
Keras深度学习实战(37)——手写文字识别
Keras深度学习实战(38)——图像字幕生成
Keras深度学习实战(39)——音乐音频分类