一、老张需求:AI作曲营建新年气氛

我有一个搞嵌入式的朋友老张,全名叫张三。是真的,他的身份证上就叫张三。据说,出生时,他爸爸妈妈预备了一堆姓名。可是两人各执一派,大打出手。吵闹声引来近邻李大爷:实在不行叫张三,我叫李四,也活得挺好。所以,针锋相对的一对年轻夫妻,给孩子上了户口,起名叫:张三。

老张长大后,一向不和李大爷说话。李大爷告知小张三:当时,假如,不是我冲进去,急中生智给你定下姓名,你或许就没命了。听到这儿,小张三才略微得以释怀,并且给李大爷磕了个头,以示感谢。李大爷说:客气了,我起姓名时,我爸爸妈妈也相同,终究仍是你爷爷给起的李四,咱两家是世交。

我打断了老张:快说,找我么事?

老张说,其实我一向觉得,我不是一般的凡人。

“嗯,你特别的烦人”。

老张说,是平凡的“凡”。我今年40岁了,哎,你知道吗?我前天刚过完40岁生日,买了一个大蛋……

“说事情!”

老张说,兄弟,帮帮忙吧,我想搞一个创造,需求你人工智能方向的帮助。

我说,啊,你又搞创造?这次什么想法。

老张说,现在快过年了,我想搞一个仿老式留声机的盒子,安装到餐厅里边。

老张说:快过年了,搞个AI作曲,用TensorFlow训练midi文件

机器选用自助形式,只需顾客付钱,盒子就会主动播映一段,由AI生成的原创新年音乐,给他们送去祝福,扫码还能够下载保存。

我问老张,你有出售渠道吗?

老张说,定心吧,餐厅我都谈好了。他们供给场地和网络,咱们只承担电费就行。

我沉思了一会,问老张:老张啊,你认识一个“耿”姓做手艺的人吗?

老张说,信任我,我必定不认识他,他的那些创造,不是没用,是真没用。

我点了点头:那就好,我支撑你!

二、midi格局:便携式音乐描绘文件

想让AI学会作曲,首先要找到一批音乐样本,让它学。

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

因而,咱们需求找一些轻松活泼的音乐,这适合新年播映。

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

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

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

老张说:快过年了,搞个AI作曲,用TensorFlow训练midi文件

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

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

老张说:快过年了,搞个AI作曲,用TensorFlow训练midi文件

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

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

电子乐器既然能够演奏音乐,那么就有曲谱。这曲谱还得有标准,由于它得在一切电子乐器中都起作用。这个“计算机能理解的曲谱”,便是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种乐器。假如不是专门剖析它,咱们还真的听不出来呐。

那么,每种乐器的音符能够获取到吗?咱们来试试:

# 承接上个代码片段,假定选定了乐器instrument
for 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 ……

通过获取instrumentnotes,能够读到此乐器的演奏信息。包括: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秒。

假如把这些数据全都打开,其实挺壮丽的,应该是如下这样:

老张说:快过年了,搞个AI作曲,用TensorFlow训练midi文件

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

这类文件要比WAVMP3这些波形文件小得多。一段30分钟钢琴曲的MIDI文件,巨细一般不超越100KB

因而,让人工智能去学习MIDI文件,并且完成主动作曲,这是一个很好的挑选。

三、实战:TensorFlow完成AI作曲

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

老张说:快过年了,搞个AI作曲,用TensorFlow训练midi文件

这批文件,除了节奏愉快适合在新年播映,还有一个特点:悉数是钢琴曲。也便是说,假如打印他的乐器的话,只有一个,那便是:Acoustic Grand Piano(原声大钢琴)。

这么做降低了样本的杂乱性,仅需求对一种乐器进行练习和猜测。同时,当它有朝一日练成了AI作曲神功,你也别梦想它会锣鼓齐鸣,它仍然只会弹钢琴。

多乐器的杂乱练习当然可行。可是现在在业内,还没有足够的数据集来支撑这件事情。

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

AI作曲,听起来很高端。其实跟文本生成、诗歌生成,没有什么区别。我之前讲过许多相关的比方《NLP实战:依据LSTM主动生成原创宋词》《NLP实战:依据GRU的主动对春联》《NLP实战:依据RNN的主动藏头诗》。也讲过许多关于NLP的知识点《NLP知识点:Tokenizer分词器》《NLP知识点:文本数据的预处理》等。假如感兴趣,咱们能够先预习一下。不看也不要紧,后边我也会简单描绘,但深度必定不如上面的专项介绍。

使用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),数据支撑便是很多成型的作品。

3.1 预备:构建数据集

首先,读取这些数据,然后把它们加工成输入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音高和startend开端时刻。其间,音高是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_midi
import tensorflow as tf
midi_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.window
notes_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~252~263~27……

然后,咱们再调用split_labels,将其悉数搞成24+1的输入输出对。此刻数据就变成了:(1~24,25)(2~25,26)……。接着,再调用batch办法,把他们搞成每64组为一个批次。这一步是结构的要求。

至此,咱们就把预备工作做好了。后边,就该交给神经网络练习去了。

3.2 练习:构建神经网络结构

这一步,咱们将构建一个神经网络模型。它将不断地由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'}。其间,stepduration是一个数就行,也便是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分类跨度较大,一旦猜测有偏差,就会导致丢失函数的值很大。而stepduration本身数值就很小,都是0.0x秒,丢失函数的值改变较小。这种不匹配,会导致后两个参数的改变被疏忽,只关心pitch的练习。因而需求降低pitch的权重平衡一下。至于详细的数值,是调试出来的。

出于讲解的需求,上面的代码仅仅是要害代码片段。文末我会把完整的项目地址公布出来,那个是能够运转的。

好了,练习上50轮,保存完成果模型。下面,就该去做猜测了。

3.3 猜测和播映:完成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个音符,猜测出来下一个音符的pitchstepduration。其他的,都是辅助操作。

咱们从资料库里,随机生成了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 = 0
midi_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作曲,用TensorFlow训练midi文件

听完了,我谈下感触吧。

怎么描绘呢?我觉得,说好听对不起良心,横竖,不难听。

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

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

做完了,我还得去找老张谈谈。

四、合作:你公然仍是这样的老张

我骑电动车去找老张,我告知他,AI作曲哥们搞定了。

老张问我,你阳了没有。

我说,没有。

老张告知我,他表弟阳了。

我说,你不用担心,究竟你们离得那么远。

老张说,他阳了后,咱们的项目也落空了。

我问为什么。

老张说:我谈好的那家饭馆,便是表弟开的。他阳了之后,现在不承认了。

我的电脑还停留在开机界面,我强制关机。走了。

我记得,上一次,我告知老张,三个月不要联络我《老张让我用TensorFlow识别语音指令》。

而这一次,我什么也没有说。就走了。

我是TF男孩,带你从IT视角看世界。