大数据文摘授权转载自数据派THU
翻译:吴金笛;校对:和中华
总览
- 学习如何开发一个自动生成音乐的端到端模型
- 理解WaveNet架构并使用Keras从零开始实现它
- 在建立自动音乐生成模型的同时,比较了WaveNet和Long-Short-Term Memory的性能
介绍
“如果我不是物理学家,我可能会成为音乐家。我经常在音乐中思考。我活在音乐的白日梦里。我从音乐的角度来看待我的生活。”——阿尔伯特·爱因斯坦
我可能不是爱因斯坦先生那样的物理学家,但我完全同意他对音乐的看法!我不记得有哪一天我没有打开音乐播放器。我上下班都伴随着音乐的旋律,老实说,它帮助我专注于工作。
我一直梦想着作曲,但却不懂乐器。直到我遇到了深度学习,这一切都成了过去。使用某些技术和框架,我能够在不了解任何乐理的情况下创作自己的原创音乐乐谱!
这是我最喜欢的专业项目之一。我结合了两个爱好——音乐和深度学习——创建了一个自动的音乐生成模型。梦想成真了!
我很高兴与你分享我的方法,包括使你也能够生成原创音乐的全部代码!首先,我们将快速理解自动音乐生成的概念,然后再深入了解用于执行此操作的不同方法。最后,我们将启用Python并设计我们自己的自动音乐生成模型。
目录
1. 什么是音乐自动生成?
2. 音乐的组成要素是什么?
3. 生成音乐的不同方法
- 使用WaveNet架构
- 使用Long-Short-Term Memory(LSTM)
4. 实现——使用python进行自动作曲
什么是音乐自动生成?
“音乐是一种艺术,是一种通用的语言。”
我把音乐定义为不同频率的音调的集合。因此,自动音乐生成是一个用最少的人为干预来创作一首短曲的过程。
产生音乐最简单的形式是什么?
这一切都是从随机选择声音并将它们组合成一段音乐开始的。1787年,莫扎特为这些随机声音的选择提出了骰子游戏。他手动合成了近272个音调!然后,他根据两个骰子点数之和选择一个音调。
另一个有趣的想法是利用音乐语法来生成音乐。
“音乐语法是指对音乐声音的合理安排和组合以及对音乐作品的正确表现所必需的知识”
-《音乐语法基础》
在20世纪50年代早期,Iannis Xenakis使用统计学和概率的概念来创作音乐——通常被称为随机音乐(Stochastic Music)。他将音乐定义为偶然出现的一系列元素(或声音)。因此,他用随机理论来表述它。他对元素的随机选择完全依赖于数学概念。
最近,深度学习架构已经成为自动音乐生成的最新技术。在本文中,我将讨论使用WaveNet和LSTM(Long-Short-Term Memory)架构来实现自动作曲的两种不同方法。
注意:本文需要对一些深度学习概念有基本的理解。我建议阅读以下文章:
- 从头开始学习卷积神经网络(CNNs)的全面教程
https://www.analyticsvidhya.com/blog/2018/12/guide-convolutional-neural-network-cnn/?utm_source=blog&utm_medium=how-to-perform-automatic-music-generation
- 深度学习要领:长短期记忆(LSTM)入门
https://www.analyticsvidhya.com/blog/2017/12/fundamentals-of-deep-learning-introduction-to-lstm/?utm_source=blog&utm_medium=how-to-perform-automatic-music-generation
- 学习序列建模的必读教程
https://www.analyticsvidhya.com/blog/2019/01/sequence-models-deeplearning/?utm_source=blog&utm_medium=how-to-perform-automatic-music-generation
音乐的组成要素是什么?
音乐本质上是由音符和和弦组成的。让我从钢琴的角度来解释这些术语:
- 音符:单键发出的声音称为音符
- 和弦:由两个或更多的键同时发出的声音称为和弦。一般来说,大多数和弦包含至少3个键音
- 八度:重复的模式称为八度。每个八度包含7个白键和5个黑键
自动生成音乐的不同方法
我将详细讨论两个基于深度学习的自动生成音乐的架构——WaveNet和LSTM。但是,为什么只有深度学习架构?
深度学习是一个受神经结构启发的机器学习领域。这些网络自动从数据集中提取特征,并能够学习任何非线性函数。这就是为什么神经网络被称为泛函逼近器(感觉泛函在数学上的定义不适合这里,网上也有人翻译为 万能函数拟逼近器,因为最早有一篇关于 Universal Approximation Theorem的论文)。
因此,深度学习模型是自然语言处理(NLP)、计算机视觉、语音合成等各个领域的最新技术。让我们来看看如何构建这些作曲模型。
方法一:使用WaveNet
“WaveNet是谷歌DeepMind开发的一种基于深度学习的原始音频生成模型。”
WaveNet的主要目的是从原始的数据分布中生成新的样本。因此,它被称为生成模型。
“WaveNet就像是NLP中的一种语言模型。”
在语言模型中,给定一个单词序列,该模型试图预测下一个单词。与语言模型类似,在WaveNet中,给定一系列样本,它试图预测下一个样本。
方法二:使用Long-Short-Term Memory(LSTM)模型
Long-Short-Term Memory (LSTM)是递归神经网络(RNNs)的一种变体,能够捕获输入序列中的长期依赖关系。LSTM在序列到序列(Seq2Seq)建模任务中有广泛的应用,如语音识别、文本摘要、视频分类等。
让我们详细讨论如何使用这两种方法训练我们的模型。
WaveNet:训练阶段
“这是一个多对一的问题,其中输入是一系列振幅值,输出是后续值。”
让我们看看如何准备输入和输出序列。
WaveNet的输入:
WaveNet将原始音频波的小块作为输入。原始音频波是指波在时间序列域中的表示。
在时间序列域中,音频波以不同时间间隔音符的振幅值的形式表示:
WaveNet的输出:
给定振幅值的序列,WaveNet试图预测连续的振幅值。
让我们通过一个示例来理解。考虑一个5秒的音频波,采样率为16,000(即每秒16,000个样本)。现在,我们有8万个样本在5秒内以不同的时间间隔记录下来。让我们把音频分成相同大小的块,比如1024(这是一个超参数)。
下图展示了模型的输入和输出序列:
前3个块的输入和输出
对于其余的块,我们可以遵循类似的过程。
从上面我们可以推断出,每个块的输出只依赖于过去的信息(即以前的时间步长),而不依赖于未来的时间步长。因此,该任务称为自回归任务,该模型称为自回归模型。
推理阶段
在推理阶段,我们将尝试生成新的样本。让我们看看怎么做:
1. 选择一个随机的样本值数组作为建模的起点
2. 现在,模型输出所有样本的概率分布
3. 选择概率最大的值并将其追加到先前的样本值数组中
4. 删除第一个元素并作为下一个迭代的输入传入模型
5. 重复步骤2和4,进行一定次数的迭代
理解WaveNet架构
WaveNet的基本结构是因果扩散的一维卷积层。首先让我们了解相关概念的重要性。
为什么使用卷积,什么是卷积?
“使用卷积的一个主要原因是从输入中提取特征。”
例如,在图像处理的情况下,用过滤器对图像进行卷积可以得到一个特征图。
卷积是一种结合了两个函数的数学运算。在图像处理的情况下,卷积是图像的某些部分与核(kernel)的线性组合。
你可以浏览下面的文章阅读更多关于卷积的知识:
- 卷积神经网络(CNNs)结构的解密
https://www.analyticsvidhya.com/blog/2017/06/architecture-of-convolutional-neural-networks-simplified-demystified/?utm_source=blog&utm_medium=how-to-perform-automatic-music-generation
什么是一维卷积?
一维卷积的目标类似于长短期记忆模型。它用于解决类似于LSTM的任务。在一维卷积中,核或者叫过滤器只沿着一个方向运动:
卷积的输出取决于核的大小、输入形状、填充类型和步长。现在,我将带领你们了解不同类型的填充来理解使用扩张的1D因果卷积层的重要性。
当我们将填充设置为valid时,输入和输出序列的长度会发生变化。输出长度小于输入长度:
当我们将填充设置为same时,在输入序列的两侧填充零以使输入和输出的长度相等:
一维卷积的优点:
- 捕获输入序列中出现的序列信息
- 与GRU或LSTM相比,训练的速度要快得多,因为它们没有循环性的连接
一维卷积的缺点:
- 当填充设置为same时,在时间步长t处的输出也与之前的t-1和未来的时间步长t 1进行卷积。因此,它违反了自回归原则
- 当填充被设置为valid时,输入和输出序列的长度会发生变化,这是计算残差连接所需要的(后面会讲到)
这为因果卷积扫清了道路。
注意: 我在这里提到的利弊是针对于此问题的。
什么是一维因果卷积?
它被定义为这样一种卷积,即t时刻的输出仅与t时刻以及前一层更早的元素进行卷积。
简单地说,正常卷积和因果卷积的区别仅仅在于填充。在因果卷积中,仅在输入序列的左边加0,以保持自回归的原则:
因果一维卷积的优点:
- 因果卷积没有考虑未来的时间步长,而这是建立生成模型的一个标准
因果一维卷积的缺点:
- 因果卷积不能回溯到序列中过去发生的时间步长。因此,因果卷积的接受域非常低。网络的接受域是指影响输出的输入数量:
如你所见,输出只受5个输入的影响。因此,网络的接受域为5,非常低。网络的接受域也可以通过增加大尺寸的核来增加,但是要记住,这样一来计算复杂度也会增加。
这将为我们引出扩张一维因果卷积的绝佳概念。
什么是扩张一维因果卷积?
“在核的值之间有孔或空缺的因果一维卷积层称为扩张的一维卷积。”
所增加的空缺数由扩张率决定。它定义了网络的接受域。大小为k、扩张率为d的核在核k的每个值之间都有d-1个孔。
如你所见,将一个3 * 3的核与一个7 * 7的输入,以扩张率为2进行卷积,最终接受域为5 * 5。
扩张一维因果卷积的优点:
- 扩张的一维卷积网络通过指数增加每一隐藏层的扩张率来增加接受域:
如你所见,输出受所有输入的影响。因此,网络的接受域为16。
WaveNet的残差块:
为了加速模型的收敛,添加了残差连接和跳跃连接的构件:
WaveNet的工作流程:
- 输入进入一个因果一维卷积
- 输出然后进入到2个不同的扩张一维卷积层并使用sigmoid和tanh激活
- 两个不同激活值逐元素相乘导致跳跃连接
- 而跳跃连接和因果一维输出的逐元素相加会导致残差
Long Short Term Memory (LSTM)方法
另一种自动生成音乐的方法是基于长短期记忆(LSTM)模型。输入和输出序列的准备类似于WaveNet。在每一个时间步长,一个振幅值被输入到长短期记忆单元-然后它计算隐藏的向量,并把它传递到下一个时间步。
基于当前的输入a(t)和先前的隐藏向量h(t-1)来计算当前时间的隐藏向量h(t)。序列信息在任何循环(一般recursive会翻译为递归)神经网络中都是这样捕获的:
LSTM的优点:
- 捕获输入序列中出现的顺序信息
LSTM的缺点:
- 由于它是按顺序处理输入信息的,所以它在训练上会花费大量的时间
实现-使用Python进行自动音乐生成
等待结束了!让我们开发一个用于自动生成音乐的端到端模型。启动你的Jupyter
notebook或Colab(或任何你喜欢的IDE)。
下载数据集:
我从众多资源中下载并组合了多个数字钢琴(译者注:Digital piano与电钢琴Electric Piano的区别在于音源的产生方式)的古典音乐文件。你可以从这里下载最终的数据集。
(https://drive.google.com/file/d/1qnQVK17DNVkU19MgVA4Vg88zRDvwCRXw/view)
导入库:
Music 21是MIT开发的用于理解音乐数据的Python库。MIDI是存储音乐文件的一种标准格式。MIDI代表乐器数字接口。MIDI文件包含说明而不是实际的音频。因此,它只占用很少的内存。这就是为什么它在传输文件时通常是首选的。
代码语言:javascript复制1. #library for understanding music
2. from music21 import *
读取音乐文件:
我们直接定义一个函数来读取MIDI文件。它返回音乐文件中存在的音符和和弦的数组。
代码语言:javascript复制1. #defining function to read MIDI files
2. def read_midi(file):
3.
4. print("Loading Music File:",file)
5.
6. notes=[]
7. notes_to_parse = None
8.
9. #parsing a midi file
10. midi = converter.parse(file)
11.
12. #grouping based on different instruments
13. s2 = instrument.partitionByInstrument(midi)
14.
15. #Looping over all the instruments
16. for part in s2.parts:
17.
18. #select elements of only piano
19. if 'Piano' in str(part):
20.
21. notes_to_parse = part.recurse()
22.
23. #finding whether a particular element is note or a chord
24. for element in notes_to_parse:
25.
26. #note
27. if isinstance(element, note.Note):
28. notes.append(str(element.pitch))
29.
30. #chord
31. elif isinstance(element, chord.Chord):
32. notes.append('.'.join(str(n) for n in element.normalOrder))
33.
34. return np.array(notes)
现在,将MIDI文件加载到我们的环境中
代码语言:javascript复制
1. #for listing down the file names
2. import os
3.
4. #Array Processing
5. import numpy as np
6.
7. #specify the path
8. path='schubert/'
9.
10. #read all the filenames
11. files=[i for i in os.listdir(path) if i.endswith(".mid")]
12.
13. #reading each midi file
14. notes_array = np.array([read_midi(path i) for i in files])
理解数据:
在本节中,我们将探索数据集并对其进行详细了解。
代码语言:javascript复制1. #converting 2D array into 1D array
2. notes_ = [element for note_ in notes_array for element in note_]
3.
4. #No. of unique notes
5. unique_notes = list(set(notes_))
6. print(len(unique_notes))
输出:304
如你所这里见,不重复音符的数量是304。现在,让我们看一下音符的分布。
代码语言:javascript复制
1. #importing library
2. from collections import Counter
3.
4. #computing frequency of each note
5. freq = dict(Counter(notes_))
6.
7. #library for visualiation
8. import matplotlib.pyplot as plt
9.
10. #consider only the frequencies
11. no=[count for _,count in freq.items()]
12.
13. #set the figure size
14. plt.figure(figsize=(5,5))
15.
16. #plot
17. plt.hist(no)
输出:
从上图可以看出,大多数音符的频率都很低。因此,我们保留最常用的音符,而忽略低频率的音符。在这里,我将阈值定义为50。不过,这个参数是可以更改的。
代码语言:javascript复制1. frequent_notes = [note_ for note_, count in freq.items() if count>=50]
2. print(len(frequent_notes))
输出:167
如你在这里看到的,经常出现的音符大约有170个。现在,让我们准备新的音乐文件,其中仅包含最常见的音符
代码语言:javascript复制1. new_music=[]
2.
3. for notes in notes_array:
4. temp=[]
5. for note_ in notes:
6. if note_ in frequent_notes:
7. temp.append(note_)
8. new_music.append(temp)
9.
10. new_music = np.array(new_music)
准备数据:
如文章中所述准备输入和输出序列:
代码语言:javascript复制1. no_of_timesteps = 32
2. x = []
3. y = []
4.
5. for note_ in new_music:
6. for i in range(0, len(note_) - no_of_timesteps, 1):
7.
8. #preparing input and output sequences
9. input_ = note_[i:i no_of_timesteps]
10. output = note_[i no_of_timesteps]
11.
12. x.append(input_)
13. y.append(output)
14.
15. x=np.array(x)
16. y=np.array(y)
现在,我们将为每个音符分配一个唯一的整数:
代码语言:javascript复制1. unique_x = list(set(x.ravel()))
2. x_note_to_int = dict((note_, number) for number, note_ in enumerate(unique_x))
我们将为输入数据准备整数序列
代码语言:javascript复制1. #preparing input sequences
2. x_seq=[]
3. for i in x:
4. temp=[]
5. for j in i:
6. #assigning unique integer to every note
7. temp.append(x_note_to_int[j])
8. x_seq.append(temp)
9.
10. x_seq = np.array(x_seq)
同样,也为输出数据准备整数序列
代码语言:javascript复制1. unique_y = list(set(y))
2. y_note_to_int = dict((note_, number) for number, note_ in enumerate(unique_y))
3. y_seq=np.array([y_note_to_int[i] for i in y])
让我们保留80%的数据用于训练,其余20%的用于评估:
代码语言:javascript复制1. from sklearn.model_selection import train_test_split
2. x_tr, x_val, y_tr, y_val = train_test_split(x_seq,y_seq,test_size=0.2,random_state=0)
构建模型
我在这里定义了2种架构– WaveNet和LSTM。请尝试两种架构,以了解WaveNet架构的重要性。
代码语言:javascript复制1. def lstm():
2. model = Sequential()
3. model.add(LSTM(128,return_sequences=True))
4. model.add(LSTM(128))
5. model.add(Dense(256))
6. model.add(Activation('relu'))
7. model.add(Dense(n_vocab))
8. model.add(Activation('softmax'))
9. model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
10. return model
我简化了WaveNet的架构,没有添加残差连接和跳跃连接,因为这些层的作用是提高收敛速度(WaveNet以原始音频波作为输入)。但在我们的例子中,输入是一组音符和和弦,因为我们在生成音乐:
代码语言:javascript复制
1. from keras.layers import *
2. from keras.models import *
3. from keras.callbacks import *
4. import keras.backend as K
5.
6. K.clear_session()
7. model = Sequential()
8.
9. #embedding layer
10. model.add(Embedding(len(unique_x), 100, input_length=32,trainable=True))
11.
12. model.add(Conv1D(64,3, padding='causal',activation='relu'))
13. model.add(Dropout(0.2))
14. model.add(MaxPool1D(2))
15.
16. model.add(Conv1D(128,3,activation='relu',dilation_rate=2,padding='causal'))
17. model.add(Dropout(0.2))
18. model.add(MaxPool1D(2))
19.
20. model.add(Conv1D(256,3,activation='relu',dilation_rate=4,padding='causal'))
21. model.add(Dropout(0.2))
22. model.add(MaxPool1D(2))
23.
24. #model.add(Conv1D(256,5,activation='relu'))
25. model.add(GlobalMaxPool1D())
26.
27. model.add(Dense(256, activation='relu'))
28. model.add(Dense(len(unique_y), activation='softmax'))
29.
30. model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
31.
32. model.summary()
定义回调以在训练期间保存最佳模型:
mc=ModelCheckpoint('best_model.h5', monitor='val_loss', mode='min', save_best_only=True,verbose=1)
让我们使用128的批大小将模型训练50个epoch:
history = model.fit(np.array(x_tr),np.array(y_tr),batch_size=128,epochs=50,validation_data=(np.array(x_val),np.array(y_val)),verbose=1, callbacks=[mc])
导入最好的模型:
代码语言:javascript复制1. #loading best model
2. from keras.models import load_model
3. model = load_model('best_model.h5')
是时候创作我们自己的音乐了。我们将按照推断阶段中提到的步骤进行预测。
代码语言:javascript复制
1. import random
2. ind = np.random.randint(0,len(x_val)-1)
3.
4. random_music = x_val[ind]
5.
6. predictions=[]
7. for i in range(10):
8.
9. random_music = random_music.reshape(1,no_of_timesteps)
10.
11. prob = model.predict(random_music)[0]
12. y_pred= np.argmax(prob,axis=0)
13. predictions.append(y_pred)
14.
15. random_music = np.insert(random_music[0],len(random_music[0]),y_pred)
16. random_music = random_music[1:]
17.
18. print(predictions)
现在,我们将整数还原为音符。
代码语言:javascript复制1. x_int_to_note = dict((number, note_) for number, note_ in enumerate(unique_x))
2. predicted_notes = [x_int_to_note[i] for i in predictions]
最后一步是将预测结果转换回MIDI文件。让我们定义一个函数来完成此任务。
代码语言:javascript复制1. def convert_to_midi(prediction_output):
2.
3. offset = 0
4. output_notes = []
5.
6. # create note and chord objects based on the values generated by the model
7. for pattern in prediction_output:
8.
9. # pattern is a chord
10. if ('.' in pattern) or pattern.isdigit():
11. notes_in_chord = pattern.split('.')
12. notes = []
13. for current_note in notes_in_chord:
14.
15. cn=int(current_note)
16. new_note = note.Note(cn)
17. new_note.storedInstrument = instrument.Piano()
18. notes.append(new_note)
19.
20. new_chord = chord.Chord(notes)
21. new_chord.offset = offset
22. output_notes.append(new_chord)
23.
24. # pattern is a note
25. else:
26.
27. new_note = note.Note(pattern)
28. new_note.offset = offset
29. new_note.storedInstrument = instrument.Piano()
30. output_notes.append(new_note)
31.
32. # increase offset each iteration so that notes do not stack
33. offset = 1
34. midi_stream = stream.Stream(output_notes)
35. midi_stream.write('midi', fp='music.mid')
将预测结果转换为音乐文件:
convert_to_midi(predicted_notes)
奥利给,对吗,但你的学习不该止步于此。请记住,我们已经构建了一个基准(baseline)模型。
有很多方法可以进一步提高模型的性能:
- 由于训练数据集的规模较小,我们可以对预训练的模型进行微调,以建立一个鲁棒的系统
- 尽可能多地收集训练数据,因为深度学习模型在更大的数据集上泛化更好
结语
深度学习在我们的日常生活中有着广泛的应用。解决任何问题的关键步骤都是理解问题陈述、阐明并定义解决问题的结构。
我在这个项目中得到了很多乐趣(和学习)。音乐是我的激情所在,将深度学习与之结合是非常有趣的。
原文链接:
https://www.analyticsvidhya.com/blog/2020/01/how-to-perform-automatic-music-generation/