想让AI学会作曲,首先要找到一批音乐样本,让它学。 AI作曲,遵循种瓜得瓜,种豆得豆的原则:你给它训练什么风格的样本,它最终就会生成什么风格的音乐。 因此,我们需要找一些轻松活泼的音乐,这适合新年播放。 音乐文件的格式,我们选择MIDI格式。MIDI的全称是:MusicalInstrumentDigitalInterface,翻译成中文就是:乐器数字接口。 这是一种什么格式?为什么会有这种格式呢? 话说,随着计算机的普及,电子乐器也出现了。电子乐器的出现,极大地节省了成本,带来了便利。 基本上有一个电子乐器,世间的乐器就都有了。 这个按钮是架子鼓,那个按钮是萨克斯。而在此之前,你想要发出这类声音,真的得敲架子鼓或者吹萨克斯管。 而且还有更为放肆的事情。你想用架子鼓一秒敲五下,得有专业的技能。但是用电子乐器,一秒敲五十下也毫不费力,因为程序就给搞定了。 这些新生事物的出现,常常让老艺术家们口吐鲜血。 电子乐器既然可以演奏音乐,那么就有乐谱。这乐谱还得有标准,因为它得在所有电子乐器中都起作用。这个计算机能理解的乐谱,就是MIDI格式。 下面我们就来解析一下MIDI文件。看看它的结构是怎么样的。 我找到一个机器猫(哆啦A梦)的主题曲,采用python做一下解析:importprettymidi加载样本文件pmprettymidi。PrettyMIDI(jqm。midi)循环乐器列表fori,instrumentinenumerate(pm。instruments):instrumentnameprettymidi。programtoinstrumentname(instrument。program)print(i,instrumentname)输出乐器名称 这个音乐,相信大家都很熟悉,就是:哦、哦、哦,哆啦A梦和我一起,让梦想发光 通过prettymidi库加载MIDI文件,获取它的乐器列表pm。instruments,打印如下: AcousticGrandPiano(原声大钢琴)、Glockenspiel(钢片琴)、StringEnsemble(弦乐合奏)、MutedTrumpet(闷音小号)、Trombone(长号)、ElectricBass(电贝斯)、AcousticGuitar(原声吉他)、Flute(长笛)、AcousticGrandPiano(原声大钢琴)、Harmonica(口琴)、Vibraphone(电颤琴)、Bagpipe(苏格兰风笛)、Marimba(馬林巴琴) 我们看到,短短一个片头曲,就动用了近20种乐器。如果不是专门分析它,我们还真的听不出来呐。 那么,每种乐器的音符可以获取到吗?我们来试试:承接上个代码片段,假设选定了乐器instrumentforj,noteinenumerate(instrument。notes):音高转音符名称notenameprettymidi。notenumbertoname(note。pitch)info(s:。2f。2f)(notename,note。start,note。end) 打印如下:AcousticGrandPiano F3:1。992。04F2:1。982。06E2:1。992。07C2:1。982。08F3:2。482。53F2:2。482。56F3:2。983。03F2:2。983。06 通过获取instrument的notes,可以读到此乐器的演奏信息。包含:pitch音高,start开始时间,end结束时间,velocity演奏力度。 名称 pitch start end velocity 示例 24hr1。98 2。03 82hr解释 音高(C1、C2) 开始时间 结束时间 力度 范围 128个音高 单位为秒 单位为秒 最高100 上面的例子中,F3:1。992。04表示:音符F3,演奏时机是从1。99秒到2。04秒。 如果把这些数据全都展开,其实挺壮观的,应该是如下这样: 其实,MIDI文件对于一首乐曲来说,就像是一个程序的源代码,也像是一副药的配方。MIDI文件里,描述了乐器的构成,以及该乐器的演奏数据。 这类文件要比WAV、MP3这些波形文件小得多。一段30分钟钢琴曲的MIDI文件,大小一般不超过100KB。 因此,让人工智能去学习MIDI文件,并且实现自动作曲,这是一个很好的选择。实战:TensorFlow实现AI作曲 我在datasets目录下,放了一批节奏欢快的MIDI文件。 这批文件,除了节奏欢快适合在新年播放,还有一个特点:全部是钢琴曲。也就是说,如果打印他的乐器的话,只有一个,那就是:AcousticGrandPiano(原声大钢琴)。 这么做降低了样本的复杂性,仅需要对一种乐器进行训练和预测。同时,当它有朝一日练成了AI作曲神功,你也别妄想它会锣鼓齐鸣,它仍然只会弹钢琴。 多乐器的复杂训练当然可行。但是目前在业内,还没有足够的数据集来支撑这件事情。 开搞之前,我们必须得先通盘考虑一下。不然,我们都不知道该把数据搞成么个形式。 AI作曲,听起来很高端。其实跟文本生成、诗歌生成,没有什么区别。我之前讲过很多相关的例子《NLP实战:基于LSTM自动生成原创宋词》《NLP实战:基于GRU的自动对春联》《NLP实战:基于RNN的自动藏头诗》。如果感兴趣,大家可以先预习一下。不看也不要紧,后面我也会简单描述,但深度肯定不如上面的专项介绍。 利用RNN,生成莎士比亚文集,是NLP领域的HelloWorld入门程序。那么,AI作曲,只不过是引入了音乐的概念。另外,在出入参数上,维度也丰富了一些。但是,从本质上讲,它还是那一套思路。 所有AI自动生成的模式,基本上都是给定一批输入值输出值。然后,让机器去学习,自己找规律。最后,学成之后,再给输入,让它自己预测出下一个值。 举个例子,莎士比亚文集的生成,样本如下:FirstCitizen: Beforeweproceedanyfurther,hearmespeak。 All: Speak,speak。 它是如何让AI训练和学习呢?其实,就是从目前的数据不断观察,观察出现下一个字符的概率。 当前 下一个 经验值 F i Fi r Fir s Firs t F后面大概率会出现i。如果现在是Fi,那么它的后面该出现r了。这些,AI作为经验记了下了。 这种记录概率的经验,在少量样本的情况下,是无意义的。 但是,当这个样本变成人类语言库的时候,那么这个概率就是语法规范,就是上帝视角。 举个例子,当我说:冬天了,窗外飘起了! 你猜,飘起了什么?是的,窗外飘起了雪。 当AI分析过人类历史上,出现过的所有语言之后。当它进行数据分析的时候,最终它会计算出:在人类的语言库里,冬天出现飘雪花的情况,要远远高于冬天飘落叶的情况。所以,它肯定也会告诉你那个空该填:雪花。 这就是AI自动作词、作曲、作画的本质。它的技术支撑是带有链式的循环神经网络(RNN),数据支撑就是大量成型的作品。准备:构建数据集 首先,读取这些数据,然后把它们加工成输入input和输出output。 展开一个MIDI文件,我们再来看一下原始数据:Note(start1。988633,end2。035121,pitch54,velocity82),Note(start1。983468,end2。060947,pitch42,velocity78),Note(start2。479335,end2。525823,pitch54,velocity82) 我们可以把前几组,比如前24组音符数据作为输入,然后第25个作为预测值。后面依次递推。把这些数据交给AI,让它研究去。 训练完成之后,我们随便给24个音符数据,让它预测第25个。然后,再拿着225,让它预测第26个,以此循环往后,连绵不绝。 这样可以吗? 可以(能训练)。但存在问题(结果非所愿)。 在使用循环神经网络的时候,前后之间要带有通用规律的联系。比如:前面有冬天做铺垫,后面遇到飘时,可以更准确地推测出来是飘雪。 我们看上面的数据,假设我们忽略velocity(力度)这个很专业的参数。仅仅看pitch音高和start、end起始时间。其中,音高是128个音符。它是普遍有规律的,值是1128,不会出圈儿。但是这个起始时间却很随机,这里可以是啊1秒开始,再往后可能就是100秒开始。 如果,我们只预测2个音符,结果200秒的时间出现的概率高。那么,第二个音符岂不是到等到3分钟后再演奏。另外,很显然演奏是有先后顺序的,因此要起止时间遵从随机的概率分布,是不靠谱的。 我觉得,一个音符会演奏多久,以及前后音符的时间间距,这两项相对来说是比较稳定的。他们更适合作为训练参数。 因此,我们决定把音符预处理成如下格式:Note(duration0。16,step0。00,pitch54),Note(duration0。56,step0。31,pitch53),Note(duration0。26,step0。22,pitch24), duration表示演奏时长,这个音符会响多久,它等于endstart。 step表示步长,本音符距离上一个出现的时间间隔,它等于start2start1。 原始数据格式〔start,end〕,同预处理后的数据格式〔duration,step〕,两者是可以做到相互转化的。 我们把所有的训练集文件整理一下:importprettymidiimporttensorflowastfmidiinputs〔〕存放所有的音符filenamestf。io。gfile。glob(datasets。midi)循环所有midi文件forfinfilenames:pmprettymidi。PrettyMIDI(f)加载一个文件instrumentspm。instruments获取乐器instrumentinstruments〔0〕取第一个乐器,此处是原声大钢琴notesinstrument。notes获取乐器的演奏数据以开始时间start做个排序。因为默认是依照end排序sortednotessorted(notes,keylambdanote:note。start)prevstartsortednotes〔0〕。start循环各项指标,取出前后关联项fornoteinsortednotes:stepnote。startprevstart此音符与上一个距离durationnote。endnote。start此音符的演奏时长prevstartnote。start此音符开始时间作为最新指标项:〔音高(音符),同前者的间隔,自身演奏的间隔〕midiinputs。append(〔note。pitch,step,duration〕) 上面的操作,是把所有的MIDI文件,依照预处理的规则,全部处理成〔pitch,step,duration〕格式,然后存放到midiinputs数组中。 这只是第一步操作。后面我们要把这个朴素的格式,拆分成输入和输出的结对。然后,转化为TensorFlow框架需要的数据集格式。seqlength24输入序列长度vocabsize128分类数量将序列拆分为输入和输出标签对defsplitlabels(sequences):inputssequences〔:1〕去掉最后一项最为输入将音高除以128,便于inputsxinputs〔vocabsize,1。0,1。0〕ysequences〔1〕截取最后一项作为输出labels{pitch:y〔0〕,step:y〔1〕,duration:y〔2〕}returninputsx,labels搞成tensor,便于流操作,比如notesds。windownotesdstf。data。Dataset。fromtensorslices(midiinputs)cutseqlengthseqlength1截取的长度,因为要拆分为输入输出,因此1每次滑动一个数据,每次截取cutseqlength个长度windowsnotesds。window(cutseqlength,shift1,stride1,dropremainderTrue)flattenlambdax:x。batch(cutseqlength,dropremainderTrue)sequenceswindows。flatmap(flatten)将25,拆分为241。24是输入,1是预测。进行训练seqdssequences。map(splitlabels,numparallelcallstf。data。AUTOTUNE)buffersizelen(midiinputs)seqlength拆分批次,缓存等优化trainds(seqds。shuffle(buffersize)。batch(64,dropremainderTrue)。cache()。prefetch(tf。data。experimental。AUTOTUNE)) 我们先分析splitlabels这个方法。它接收一段序列数组。然后将其分为两段,最后1项作为后段,其余部分作为前段。 我们把seqlength定义为24,从总数据midiinputs中,利用notesds。window实现每次取25个数据,取完了向后移动1格,再继续取数据。直到凑不齐25个数据(dropremainderTrue意思是不足25弃掉)停止。 至此,我们就有了一大批以25为单位的数据组。其实,他们是:125、226、327 然后,我们再调用splitlabels,将其全部搞成241的输入输出对。此时数据就变成了:(124,25)、(225,26)。接着,再调用batch方法,把他们搞成每64组为一个批次。这一步是框架的要求。 至此,我们就把准备工作做好了。后面,就该交给神经网络训练去了。训练:构建神经网络结构 这一步,我们将构建一个神经网络模型。它将不断地由24个音符观察下一个出现的音符。它记录,它思考,它尝试推断,它默写并对照答案。一旦见得多了,量变就会引起质变,它将从整个音乐库的角度,给出作曲的最优解。 好了,上代码:inputshape(seqlength,3)输入形状inputstf。keras。Input(inputshape)xtf。keras。layers。LSTM(128)(inputs)输出形状outputs{pitch:tf。keras。layers。Dense(128,namepitch)(x),step:tf。keras。layers。Dense(1,namestep)(x),duration:tf。keras。layers。Dense(1,nameduration)(x),}modeltf。keras。Model(inputs,outputs) 上面代码我们定义了输入和输出的格式,然后中间加了个LSTM层。 先说输入。因为我们给的格式是〔音高,间隔,时长〕共3个关键指标。而且每24个音,预测下一个音。所以inputshape(24,3)。 再说输出。我们最终期望AI可以自动预测音符,当然要包含音符的要素,那也就是outputs{pitch,step,duration}。其中,step和duration是一个数就行,也就是Dense(1)。但是,pitch却不同,它需要是128个音符中的一个。因此,它是Dense(128)。 最后说中间层。我们期望有人能将输入转为输出,而且最好还有记忆。前后之间要能综合起来,要根据前面的铺垫,后面给出带有相关性的预测。那么,这个长短期记忆网络LSTM(LongShortTermMemory)就是最佳的选择了。 最终,model。summary()打印结构如下所示: Layer(type) OutputShape Param Connectedto input(InputLayer) 〔(None,24,3)〕 0hr〔〕 lstm(LSTM) (None,128) 67584hr〔input〔0〕〔0〕〕 duration(Dense) (None,1) 129hr〔lstm〔0〕〔0〕〕 pitch(Dense) (None,128) 16512hr〔lstm〔0〕〔0〕〕 step(Dense) (None,1) 129hr〔lstm〔0〕〔0〕〕 Totalparams:84,354 后面,配置训练参数,开始训练:checkpointpathmodelmodel。ckpt模型存放路径model。compile(配置参数lossloss,lossweights{pitch:0。05,step:1。0,duration:1。0},optimizertf。keras。optimizers。Adam(learningrate0。01),)模型保存参数cpcallbacktf。keras。callbacks。ModelCheckpoint(filepathcheckpointpath,saveweightsonlyTrue,savebestonlyTrue)启动训练,训练50个周期model。fit(trainds,validationdatatrainds,epochs50,callbacks〔cpcallback〕) 训练完成之后,会将模型保存在modelmodel。ckpt目录下。而且,我们设置了只保存最优的一个模型savebestonlyTrue。 上面有个需要特别说明的地方,那就是在model。compile中,给损失函数加了一个权重lossweights的配置。这是因为,在输出的三个参数中,pitch音高的128分类跨度较大,一旦预测有偏差,就会导致损失函数的值很大。而step和duration本身数值就很小,都是0。0x秒,损失函数的值变化较小。这种不匹配,会导致后两个参数的变化被忽略,只关心pitch的训练。因此需要降低pitch的权重平衡一下。至于具体的数值,是调试出来的。 出于讲解的需要,上面的代码仅仅是关键代码片段。文末我会把完整的项目地址公布出来,那个是可以运行的。 好了,训练上50轮,保存完结果模型。下面,就该去做预测了。预测和播放:实现AI作曲 现在这个模型,已经可以根据24个音符去推测出下一个音符了。我们来试一下。加载模型ifos。path。exists(checkpointpath。index):model。loadweights(checkpointpath)从音符库中随机拿出24个音符,当然你也可以自己编samplenotesrandom。sample(midiinputs,24)numpredictions600预测后面600个循环600次,每次取最新的24个foriinrange(numpredictions):拿出最后24个nnotessamplenotes〔seqlength:〕主要给音高做一个128分类归一化notes〔〕forinputinnnotes:notes。append(〔input〔0〕vocabsize,input〔1〕,input〔2〕〕)将24个音符交给模型预测predictionsmodel。predict(〔notes〕)取出预测结果pitchlogitspredictions〔pitch〕pitchtf。random。categorical(pitchlogits,numsamples1)〔0〕steppredictions〔step〕〔0〕durationpredictions〔duration〕〔0〕pitch,step,durationint(pitch),float(step),float(duration)将预测值添加到音符数组中,进行下一次循环samplenotes。append(〔pitch,step,duration〕) 其实,关键代码就一句predictionsmodel。predict(〔notes〕)。根据24个音符,预测出来下一个音符的pitch、step和duration。其他的,都是辅助操作。 我们从素材库里,随机生成了24个音符。其实,如果你懂声乐,你也可以自己编24个音符。这样,起码能给音乐定个基调。因为,后面的预测都是根据前面特征来的。当然,也可以不是24个,根据2个生成1个也行。那前提是,训练的时候也得是21的模式。但是,我感觉还是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〕〕 但是,这是预处理后的特征,并非是可以直接演奏的音符。是否还记得durationendstart以及stepstart2start1。我们需要把它们还原成为MIDI体系下的属性:复原midi数据prevstart0midinotes〔〕forminsamplenotes:pitch,step,durationmstartprevstartstependstartdurationprevstartstartmidinotes。append(〔pitch,start,end〕) 这样,就把〔pitch,step,duration〕转化成了〔pitch,start,end〕。打印midinotes如下:〔〔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文件pmprettymidi。PrettyMIDI()instrumentprettymidi。Instrument(programprettymidi。instrumentnametoprogram(AcousticGrandPiano))forninmidinotes:noteprettymidi。Note(velocity100,pitchn〔0〕,startn〔1〕,endn〔2〕)instrument。notes。append(note)pm。instruments。append(instrument)pm。write(out。midi) MIDI文件有5个必需的要素。其中,乐器我们设置为AcousticGrandPiano原声大钢琴。velocity没有参与训练,但也需要,我们设为固定值100。其他的3个参数,都是AI生成的,依次代入。最后,把结果生成到out。midi文件中。 使用Window自带的MediaPlayer就可以直接播放这个文件。你听不到,我可以替你听一听。 听完了,我谈下感受吧。 怎么描述呢?我觉得,说好听对不起良心,反正,不难听。 好了,AI作曲就到此为止了。 源代码已上传到GitHub地址是:https:github。comhlwgyaimusic。