AI作曲,遵照“种瓜得瓜,种豆得豆”的原则:你给它演习什么风格的样本,它终极就会天生什么风格的音乐。

用TensorFlow演习midi文件实现AI作曲_音符_数据 智能写作

因此,我们须要找一些轻松活泼的音乐,这适宜新年播放。

音乐文件的格式,我们选择MIDI格式。
MIDI的全称是:Musical Instrument Digital Interface,翻译成中文便是:乐器数字接口。

这是一种什么格式?为什么会有这种格式呢?

话说,随着打算机的遍及,电子乐器也涌现了。
电子乐器的涌现,极大地节省了本钱,带来了便利。

基本上有一个电子乐器,世间的乐器就都有了。

这个按钮是架子鼓,那个按钮是萨克斯。
而在此之前,你想要发出这类声音,真的得敲架子鼓或者吹萨克斯管。

而且还有更为放荡的事情。
你想用架子鼓一秒敲五下,得有专业的技能。
但是用电子乐器,一秒敲五十下也绝不费力,由于程序就给搞定了。

这些新生事物的涌现,常常让老艺术家们口吐鲜血。

电子乐器既然可以演奏音乐,那么就有乐谱。
这乐谱还得有标准,由于它得在所有电子乐器中都起浸染。
这个“打算性能理解的乐谱”,便是MIDI格式。

下面我们就来解析一下MIDI文件。
看看它的构造是怎么样的。

我找到一个机器猫(哆啦A梦)的主题曲,采取python做一下解析:

import pretty_midi# 加载样本文件pm = pretty_midi.PrettyMIDI("jqm.midi") # 循环乐器列表for i, instrument in enumerate(pm.instruments): instrument_name = pretty_midi.program_to_instrument_name(instrument.program) print(i, instrument_name) # 输出乐器名称

这个音乐,相信大家都很熟习,便是:哦、哦、哦,哆啦A梦和我一起,让梦想发光……

通过pretty_midi库加载MIDI文件,获取它的乐器列表pm.instruments,打印如下:

Acoustic Grand Piano(原声大钢琴)、Glockenspiel(钢片琴)、String Ensemble(弦乐合奏) 、Muted Trumpet(闷音小号)、Trombone(长号)、Electric Bass(电贝斯)、Acoustic Guitar(原声吉他)、Flute(长笛)、Acoustic Grand Piano(原声大钢琴)、Harmonica(口琴)、Vibraphone(电颤琴)、Bagpipe(苏格兰风笛)、Marimba(馬林巴琴)……

我们看到,短短一个片头曲,就动用了近20种乐器。
如果不是专门剖析它,我们还真的听不出来呐。

那么,每种乐器的音符可以获取到吗?我们来试试:

# 承接上个代码片段,假设选定了乐器instrumentfor j, note in enumerate(instrument.notes): # 音高转音符名称 note_name = pretty_midi.note_number_to_name(note.pitch) info = ("%s:%.2f->%.2f")%(note_name, note.start, note.end)

打印如下:

Acoustic Grand Piano

F#3:1.99->2.04 F#2:1.98->2.06 E2:1.99->2.07 C2:1.98->2.08 F#3:2.48->2.53 F#2:2.48->2.56 F#3:2.98->3.03 F#2:2.98->3.06 ……

通过获取instrument的notes,可以读到此乐器的演奏信息。
包含:pitch音高,start开始韶光,end结束韶光,velocity演奏力度。

名称

pitch

start

end

velocity

示例

24

1.98

2.03

82

阐明

音高(C1、C2)

开始韶光

结束韶光

力度

范围

128个音高

单位为秒

单位为秒

最高100

上面的例子中,F#3:1.99->2.04表示:音符F#3,演奏机遇是从1.99秒到2.04秒。

如果把这些数据全都展开,实在挺壮不雅观的,该当是如下这样:

实在,MIDI文件对付一首乐曲来说,就像是一个程序的源代码,也像是一副药的配方。
MIDI文件里,描述了乐器的构成,以及该乐器的演奏数据。

这类文件要比WAV、MP3这些波形文件小得多。
一段30分钟钢琴曲的MIDI文件,大小一样平常不超过100KB。

因此,让人工智能去学习MIDI文件,并且实现自动作曲,这是一个很好的选择。

实战:TensorFlow实现AI作曲

我在datasets目录下,放了一批节奏欢畅的MIDI文件。

这批文件,除了节奏欢畅适宜在新年播放,还有一个特点:全部是钢琴曲。
也便是说,如果打印他的乐器的话,只有一个,那便是:Acoustic Grand Piano(原声大钢琴)。

这么做降落了样本的繁芜性,仅须要对一种乐器进行演习和预测。
同时,当它有朝一日练成了AI作曲神功,你也别企图它会锣鼓齐鸣,它仍旧只会弹钢琴。

多乐器的繁芜演习当然可行。
但是目前在业内,还没有足够的数据集来支撑这件事情。

开搞之前,我们必须得先通盘考虑一下。
不然,我们都不知道该把数据搞成么个形式。

AI作曲,听起来很高端。
实在跟文本天生、诗歌天生,没有什么差异。
我之前讲过很多干系的例子《NLP实战:基于LSTM自动天生原创宋词》《NLP实战:基于GRU的自动对春联》《NLP实战:基于RNN的自动藏头诗》。
如果感兴趣,大家可以先预习一下。
不看也不要紧,后面我也会大略描述,但深度肯定不如上面的专项先容。

利用RNN,天生莎士比亚文集,是NLP领域的HelloWorld入门程序。
那么,AI作曲,只不过是引入了音乐的观点。
其余,在出入参数上,维度也丰富了一些。
但是,从实质上讲,它还是那一套思路。

所有AI自动天生的模式,基本上都是给定一批输入值+输出值。
然后,让机器去学习,自己找规律。
末了,学成之后,再给输入,让它自己预测出下一个值。

举个例子,莎士比亚文集的天生,样本如下:

First Citizen:Before we proceed any further, hear me speak.

All:Speak, speak.

它是如何让AI演习和学习呢?实在,便是从目前的数据不断不雅观察,不雅观察涌现下一个字符的概率。

当前

下一个

履历值

F

i

Fi

r

Fir

s

Firs

t

☆☆

……

……

……

F后面大概率会涌现i。
如果现在是Fi,那么它的后面该涌现r了。
这些,AI作为履历记了下了。

这种记录概率的履历,在少量样本的情形下,是无意义的。

但是,当这个样本变成人类措辞库的时候,那么这个概率便是语法规范,便是上帝视角。

举个例子,当我说:冬天了,窗外飘起了__!

你猜,飘起了什么?是的,窗外飘起了雪。

当AI剖析过人类历史上,涌现过的所有措辞之后。
当它进行数据剖析的时候,终极它司帐算出:在人类的措辞库里,冬天涌现飘雪花的情形,要远远高于冬天飘落叶的情形。
以是,它肯定也会见告你那个空该填:雪花。

这便是AI自动作词、作曲、作画的实质。
它的技能支撑是带有链式的循环神经网络(RNN),数据支撑便是大量成型的作品。

准备:构建数据集

首先,读取这些数据,然后把它们加工成输入input和输出output。

展开一个MIDI文件,我们再来看一下原始数据:

Note(start=1.988633, end=2.035121, pitch=54, velocity=82),Note(start=1.983468, end=2.060947, pitch=42, velocity=78),Note(start=2.479335, end=2.525823, pitch=54, velocity=82)……

我们可以把前几组,比如前24组音符数据作为输入,然后第25个作为预测值。
后面依次递推。
把这些数据交给AI,让它研究去。

演习完成之后,我们随便给24个音符数据,让它预测第25个。
然后,再拿着2~25,让它预测第26个,以此循环今后,连绵不绝。

这样可以吗?

可以(能演习)。
但存在问题(结果非所愿)。

在利用循环神经网络的时候,前后之间要带有通用规律的联系。
比如:前面有“冬天”做铺垫,后面碰着“飘”时,可以更准确地推测出来是“飘雪”。

我们看上面的数据,假设我们忽略velocity(力度)这个很专业的参数。
仅仅看pitch音高和start、end起始韶光。
个中,音高是128个音符。
它是普遍有规律的,值是1~128,不会出圈儿。
但是这个起始韶光却很随机,这里可以是啊1秒开始,再今后可能便是100秒开始。

如果,我们只预测2个音符,结果200秒的韶光涌现的概率高。
那么,第二个音符岂不是到等到3分钟后再演奏。
其余,很显然演奏是有先后顺序的,因此要起止韶光屈服随机的概率分布,是不靠谱的。

我以为,一个音符会演奏多久,以及前后音符的韶光间距,这两项相对来说是比较稳定的。
他们更适宜作为演习参数。

因此,我们决定把音符预处理成如下格式:

Note(duration=0.16, step=0.00, pitch=54),Note(duration=0.56, step=0.31, pitch=53),Note(duration=0.26, step=0.22, pitch=24),……

duration表示演奏时长,这个音符会响多久,它即是end-start。

step表示步长,本音符间隔上一个涌现的韶光间隔,它即是start2-start1。

原始数据格式[start,end],同预处理后的数据格式[duration,step],两者是可以做到相互转化的。

我们把所有的演习集文件整理一下:

import pretty_midiimport tensorflow as tfmidi_inputs = [] # 存放所有的音符filenames = tf.io.gfile.glob("datasets/.midi")# 循环所有midi文件for f in filenames: pm = pretty_midi.PrettyMIDI(f) # 加载一个文件 instruments = pm.instruments # 获取乐器 instrument = instruments[0] # 取第一个乐器,此处是原声大钢琴 notes = instrument.notes # 获取乐器的演奏数据 # 以开始韶光start做个排序。
由于默认是依照end排序 sorted_notes = sorted(notes, key=lambda note: note.start) prev_start = sorted_notes[0].start # 循环各项指标,取出前后关联项 for note in sorted_notes: step = note.start - prev_start # 此音符与上一个间隔 duration = note.end - note.start # 此音符的演奏时长 prev_start = note.start # 此音符开始韶光作为最新 # 指标项:[音高(音符),同前者的间隔,自身演奏的间隔] midi_inputs.append([note.pitch, step, duration])

上面的操作,是把所有的MIDI文件,依照预处理的规则,全部处理成[pitch, step, duration]格式,然后存放到midi_inputs数组中。

这只是第一步操作。
后面我们要把这个朴素的格式,拆分成输入和输出的结对。
然后,转化为TensorFlow框架须要的数据集格式。

seq_length = 24 # 输入序列长度vocab_size = 128 # 分类数量# 将序列拆分为输入和输出标签对def split_labels(sequences): inputs = sequences[:-1] # 去掉末了一项最为输入 # 将音高除以128,便于 inputs_x = inputs/[vocab_size,1.0,1.0] y = sequences[-1] # 截取末了一项作为输出 labels = {"pitch":y[0], "step":y[1],"duration":y[2]} return inputs_x, labels# 搞成tensor,便于流操作,比如notes_ds.windownotes_ds = tf.data.Dataset.from_tensor_slices(midi_inputs)cut_seq_length = seq_length+1 # 截取的长度,由于要拆分为输入+输出,因此+1# 每次滑动一个数据,每次截取cut_seq_length个长度windows = notes_ds.window(cut_seq_length, shift=1, stride=1,drop_remainder=True)flatten = lambda x: x.batch(cut_seq_length, drop_remainder=True)sequences = windows.flat_map(flatten)# 将25,拆分为24+1。
24是输入,1是预测。
进行演习seq_ds = sequences.map(split_labels, num_parallel_calls=tf.data.AUTOTUNE)buffer_size = len(midi_inputs) - seq_length# 拆分批次,缓存等优化train_ds = (seq_ds.shuffle(buffer_size) .batch(64, drop_remainder=True) .cache().prefetch(tf.data.experimental.AUTOTUNE))

我们先剖析split_labels这个方法。
它吸收一段序列数组。
然后将其分为两段,末了1项作为后段,别的部分作为前段。

我们把seq_length定义为24,从总数据midi_inputs中,利用notes_ds.window实现每次取25个数据,取完了向后移动1格,再连续取数据。
直到凑不齐25个数据(drop_remainder=True意思是不敷25弃掉)停滞。

至此,我们就有了一大批以25为单位的数据组。
实在,他们是:1~25、2~26、3~27……

然后,我们再调用split_labels,将其全部搞成24+1的输入输出对。
此时数据就变成了:(1~24,25)、(2~25,26)……。
接着,再调用batch方法,把他们搞成每64组为一个批次。
这一步是框架的哀求。

至此,我们就把准备事情做好了。
后面,就该交给神经网络演习去了。

演习:构建神经网络构造

这一步,我们将构建一个神经网络模型。
它将不断地由24个音符不雅观察下一个涌现的音符。
它记录,它思考,它考试测验推断,它默写并对照答案。
一旦见得多了,量变就会引起质变,它将从全体音乐库的角度,给出作曲的最优解。

好了,上代码:

input_shape = (seq_length, 3) # 输入形状inputs = tf.keras.Input(input_shape)x = tf.keras.layers.LSTM(128)(inputs)# 输出形状outputs = { 'pitch': tf.keras.layers.Dense(128, name='pitch')(x), 'step': tf.keras.layers.Dense(1, name='step')(x), 'duration': tf.keras.layers.Dense(1, name='duration')(x),}model = tf.keras.Model(inputs, outputs)

上面代码我们定义了输入和输出的格式,然后中间加了个LSTM层。

先说输入。
由于我们给的格式是[音高,间隔,时长]共3个关键指标。
而且每24个音,预测下一个音。
以是input_shape = (24, 3)。

再说输出。
我们终极期望AI可以自动预测音符,当然要包含音符的要素,那也便是outputs = {'pitch','step','duration'}。
个中,step和duration是一个数就行,也便是Dense(1)。
但是,pitch却不同,它须要是128个音符中的一个。
因此,它是Dense(128)。

末了说中间层。
我们期望有人能将输入转为输出,而且最好还有影象。
前后之间要能综合起来,要根据前面的铺垫,后面给出带有干系性的预测。
那么,这个是非期影象网络LSTM(Long Short-Term Memory)便是最佳的选择了。

终极,model.summary()打印构造如下所示:

Layer (type)

Output Shape

Param

Connected to

input (InputLayer)

[(None, 24, 3)]

0

[]

lstm (LSTM)

(None, 128)

67584

['input[0][0]']

duration (Dense)

(None, 1)

129

['lstm[0][0]']

pitch (Dense)

(None, 128)

16512

['lstm[0][0]']

step (Dense)

(None, 1)

129

['lstm[0][0]']

Total params: 84,354

后面,配置演习参数,开始演习:

checkpoint_path = 'model/model.ckpt' # 模型存放路径model.compile( # 配置参数 loss=loss, loss_weights={'pitch': 0.05,'step': 1.0,'duration':1.0}, optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),)# 模型保存参数cp_callback = tf.keras.callbacks.ModelCheckpoint( filepath=checkpoint_path ,save_weights_only=True, save_best_only=True)# 启动演习,演习50个周期model.fit(train_ds, validation_data=train_ds , epochs=50,callbacks=[cp_callback])

演习完成之后,会将模型保存在'model/model.ckpt'目录下。
而且,我们设置了只保存最优的一个模型save_best_only=True。

上面有个须要特殊解释的地方,那便是在model.compile中,给丢失函数加了一个权重loss_weights的配置。
这是由于,在输出的三个参数中,pitch音高的128分类跨度较大,一旦预测有偏差,就会导致丢失函数的值很大。
而step和duration本身数值就很小,都是0.0x秒,丢失函数的值变革较小。
这种不匹配,会导致后两个参数的变革被忽略,只关心pitch的演习。
因此须要降落pitch的权重平衡一下。
至于详细的数值,是调试出来的。

出于讲解的须要,上面的代码仅仅是关键代码片段。
文末我会把完全的项目地址公布出来,那个是可以运行的。

好了,演习上50轮,保存完结果模型。
下面,就该去做预测了。

预测和播放:实现AI作曲

现在这个模型,已经可以根据24个音符去推测出下一个音符了。
我们来试一下。

# 加载模型if os.path.exists(checkpoint_path + '.index'): model.load_weights(checkpoint_path) # 从音符库中随机拿出24个音符,当然你也可以自己编sample_notes = random.sample(midi_inputs, 24)num_predictions = 600 # 预测后面600个# 循环600次,每次取最新的24个for i in range(num_predictions): # 拿出末了24个 n_notes = sample_notes[-seq_length:] # 紧张给音高做一个128分类归一化 notes = [] for input in n_notes: notes.append([input[0]/vocab_size,input[1],input[2]]) # 将24个音符交给模型预测 predictions = model.predict([notes]) # 取出预测结果 pitch_logits = predictions['pitch'] pitch = tf.random.categorical(pitch_logits, num_samples=1)[0] step = predictions['step'][0] duration = predictions['duration'][0] pitch, step, duration = int(pitch), float(step), float(duration) # 将预测值添加到音符数组中,进行下一次循环 sample_notes.append([pitch, step, duration])

实在,关键代码就一句predictions = model.predict([notes])。
根据24个音符,预测出来下一个音符的pitch、step和duration。
其他的,都是赞助操作。

我们从素材库里,随机天生了24个音符。
实在,如果你懂声乐,你也可以自己编24个音符。
这样,最少能给音乐定个基调。
由于,后面的预测都是根据前面特色来的。
当然,也可以不是24个,根据2个天生1个也行。
那条件是,演习的时候也得是2+1的模式。
但是,我觉得还是24个好,感情更深一些。

从24个天生1个后,变成了25个。
然后再取这25个中的末了24个,连续天生下一个。
循环600次,末了天生了624个音符。
打印一下:

[[48, 0.001302083333371229, 0.010416666666628771], [65, 0.11979166666674246, 0.08463541666651508]……[72, 0.03634712100028992, 0.023365378379821777], [41, 0.04531348496675491, 0.011086761951446533]]

但是,这是预处理后的特色,并非是可以直接演奏的音符。
是否还记得duration = end-start以及step=start2-start1。
我们须要把它们还原成为MIDI体系下的属性:

# 复原midi数据prev_start = 0midi_notes = []for m in sample_notes: pitch, step, duration = m start = prev_start + step end = start + duration prev_start = start midi_notes.append([pitch, start, end])

这样,就把[pitch, step, duration]转化成了[pitch, start, end]。
打印midi_notes如下:

[[48, 0.001302083333371229, 0.01171875], [65, 0.12109375000011369, 0.20572916666662877], ……[72, 32.04372873653976, 32.067094114919584], [41, 32.08904222150652, 32.100128983457964]]

我们从数据可以看到,末了播放到了32秒。
也就说我们AI天生的这段600多个音符的乐曲,可以播放32秒。

听一听效果,那就把它写入MIDI文件吧。

# 写入midi文件pm = pretty_midi.PrettyMIDI()instrument = pretty_midi.Instrument( program=pretty_midi.instrument_name_to_program("Acoustic Grand Piano"))for n in midi_notes: note = pretty_midi.Note(velocity=100,pitch=n[0],start=n[1],end=n[2]) instrument.notes.append(note)pm.instruments.append(instrument)pm.write("out.midi")

MIDI文件有5个必需的要素。
个中,乐器我们设置为"Acoustic Grand Piano"原声大钢琴。
velocity没有参与演习,但也须要,我们设为固定值100。
其他的3个参数,都是AI天生的,依次代入。
末了,把结果天生到out.midi文件中。

利用Window自带的Media Player就可以直接播放这个文件。
你听不到,我可以替你听一听。

听完了,我谈下感想熏染吧。

怎么描述呢?我以为,说好听对不起良心,反正,不难听。

好了,AI作曲就到此为止了。

源代码已上传到GitHub地址是:https://github.com/hlwgy/ai_music。