☰
基于LSTM的自动音乐生成:从MIDI预处理到模型训练与采样实践
2026/10/7 13:20:29 网站建设 项目流程

简介:这份压缩包是一个基于机器学习LSTM的自动音乐生成软件完整项目,包含Python源码、项目说明与详细设计说明书,适合作为课程设计或期末大作业直接使用。项目已获导师指导并以97分高分通过,代码与文档完整,下载后无需修改即可运行。压缩包共6个文件,其中具体包含2个Jupyter Notebook(分别为主程序与用户界面)、1个Python生成脚本、1份PDF详细设计说明书,以及许可与说明文档,整体大小仅462KB。目前已有296人浏览学习。资源内附完整可运行源码、设计说明和使用文档,覆盖古典钢琴音乐自动生成场景,可帮助读者系统理解LSTM在序列生成方向的应用,包括数据预处理、模型训练、音乐生成与结果展示等核心环节,既可直接用于课程设计提交,也可作为后续二次开发的起点与参考。

1. 基于机器学习LSTM的自动音乐生成:这份源码包能直接跑通,但你要知道它怎么工作

机器学习里的LSTM(长短期记忆网络)在很多人的印象中是做股票预测、时间序列回归的,但拿它来生成音乐同样是一条被验证过的路子。这个项目把一个重要的方向做了落地:让模型学习古典钢琴曲的旋律规律,然后自己续写新音符。它不是简单调用某个现成模型,而是从MIDI数据预处理、LSTM模型搭建、训练到生成完整走了一遍。我拆完这份源码之后的第一感受是:作为课程设计,它的完成度确实高;作为入门LSTM序列生成的学习样例,它的代码结构也足够清晰,新手跟着跑一遍,能同时搞明白“序列数据怎么喂给LSTM”和“音乐生成到底在生成什么”这两件事。适合正在做机器学习课程设计、期末大作业,或者想用Python实践一个完整LSTM落地项目的读者。

2. 项目结构与MIDI数据预处理:把钢琴曲转成LSTM能读懂的序列

2.1 这份压缩包里到底有什么

拿到压缩包之后,第一步不是急着跑模型,而是搞清楚文件结构。解压后你会看到这些核心内容:

文件/目录作用
main.ipynb主流程notebook,从数据预处理到训练的全过程
UI.ipynb带界面交互的演示notebook
create_music_py.py纯Python脚本版音乐生成入口
Classical-piano-composer训练用的古典钢琴MIDI数据集目录
详细设计说明书.pdf课程设计配套文档,包含系统设计、流程、测试
README.md项目说明

这里要特别提醒:Classical-piano-composer这个数据集目录是项目能不能跑起来的关键。很多课程设计项目会把数据集放在网盘里,下载后还要手动改路径,这份资源是直接打包好的,省了一步。

2.2 MIDI文件的读取方式

音乐生成的第一步是把MIDI文件解析成模型能理解的数据。MIDI不是音频文件,它记录的是音符的演奏指令——哪个时间点、按下哪个音、持续多久、力度多大。项目里用music21这个库来做解析,这是音乐信息领域的主流工具。

from music21 import converter, instrument, note, chord def parse_midi(file_path): """解析MIDI文件,提取音符和和弦序列""" midi = converter.parse(file_path) notes_to_parse = None # 提取所有乐器声部,找钢琴部分 parts = instrument.partitionByInstrument(midi) if parts: notes_to_parse = parts.parts[0].recurse() else: notes_to_parse = midi.flat.notes notes = [] for element in notes_to_parse: if isinstance(element, note.Note): # 单音:用音名表示,如 C4、E5 notes.append(str(element.pitch)) elif isinstance(element, chord.Chord): # 和弦:用'.'连接多个音名,如 C4.E4.G4 notes.append('.'.join(str(n) for n in element.pitches)) return notes

这段代码的逻辑很直接:先读MIDI文件,尝试按乐器分组,优先取钢琴声部;如果没有分组信息,就把所有音符拉平处理。需要留意的是,这个项目处理的曲子是单声部旋律为主,和弦会以.连接的方式压缩成一个符号,这样LSTM能把和弦当作一个token来学习,而不是把一个和弦拆成多个独立音高。

2.3 序列化与映射:把音乐变成数字

解析出来的是一串字符串列表,但神经网络只能吃数值。项目里会把所有出现过的音符符号收集起来,建立一张“符号到整数”的映射表,然后按固定长度切窗生成训练样本。

from collections import Counter def create_sequences(notes, sequence_length=100): """把音符序列切成长度为sequence_length的输入-输出对""" # 建立音符合表到整数的映射 symbol_to_int = {sym: idx for idx, sym in enumerate(sorted(set(notes)))} sequences = [] # 滑动窗口切分:用前100个音符预测第101个 for i in range(0, len(notes) - sequence_length, 1): seq_in = notes[i:i + sequence_length] seq_out = notes[i + sequence_length] sequences.append((seq_in, seq_out)) return sequences, symbol_to_int

这里的sequence_length=100是一个关键超参数。100个音符的上下文够LSTM捕捉到乐句级别的重复结构,但又不至于太长导致训练缓慢。如果数据集比较小,可以适当减小到50或者64;我之前在类似项目上测试过,短序列会让生成结果偏向碎片化,长序列会让模型更容易过拟合到某几首曲子的固定模式。

2.4 数据集规模与训练前的准备工作

项目自带的Classical-piano-composer目录里,大多是古典钢琴曲目的MIDI文件。从实际跑通的经验来看,这个数据集的量级在几十到上百首曲目之间,预处理后得到的音符总数通常在数万级别。这个规模对于LSTM来说不算大,训练时要注意过拟合问题,后面我会专门讲。

训练之前还需要把输入序列做one-hot编码或者Embedding映射。在notebook里,作者用的是one-hot方式,也就是每个时间步输入一个向量,维度等于音符合表总数。这种方式在小词表场景下比Embedding更直接,虽然稀疏,但效果足够。

import numpy as np def prepare_training_data(sequences, symbol_to_int, sequence_length=100): """把字符串序列转成one-hot编码的numpy数组""" vocab_size = len(symbol_to_int) x = np.zeros((len(sequences), sequence_length, vocab_size), dtype=bool) y = np.zeros((len(sequences), vocab_size), dtype=bool) for i, (seq_in, seq_out) in enumerate(sequences): for j, symbol in enumerate(seq_in): x[i, j, symbol_to_int[symbol]] = 1 y[i, symbol_to_int[seq_out]] = 1 return x, y

dtype=bool在这里是合理的——One-hot向量非0即1,用bool类型比float省4倍内存。这个细节在序列长、词表大的情况下能明显降低内存占用。

3. LSTM模型架构与训练细节:从Keras搭建到loss收敛

3.1 为什么音乐生成选择LSTM而不是CNN或Transformer

音乐是时序数据,一个音符出现的概率强烈依赖前面的音符序列。LSTM的门控机制允许信息在跨时间步传播时进行选择性遗忘和记忆,这对学习音乐中的主题重复、乐句呼应特别关键。

CNN在音乐生成里也有应用,但它在短窗口特征提取上有优势,在处理长距离依赖时感受野有限。Transformer理论上更强大,但它需要大量数据和计算资源,对课程设计场景来说,LSTM在“数据量中等、单机CPU可训练、效果可感知”这几个条件之间是最佳平衡点。项目选择Keras构建LSTM,实验成本低,调整参数也很方便。

from keras.models import Sequential from keras.layers import LSTM, Dense, Dropout, Activation def build_model(input_shape, vocab_size): """构建LSTM音乐生成模型""" model = Sequential([ LSTM(256, input_shape=input_shape, return_sequences=True), Dropout(0.3), LSTM(256), Dropout(0.3), Dense(vocab_size), Activation('softmax') ]) model.compile( loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'] ) return model

这里用了两层LSTM,第一层设置了return_sequences=True,意思是把每个时间步的隐藏状态都输出给下一层,这样第二层LSTM能看到完整的中间表示。Dropout(0.3)用来缓解过拟合,但在音乐生成这类任务上,dropout比例不宜太高,0.2到0.3是常见区间,太高的dropout会让模型学不稳。

rmsprop优化器是这个任务里非常经典的选择——它自适应调整学习率,在RNN上通常比Adam更稳,尤其是在后期收敛阶段。

3.2 训练过程的参数选择与收敛判断

训练时用到的关键参数会直接影响结果质量。我拆阅项目后整理了最核心的几个:

参数值说明
batch_size128序列样本数较大时用分批训练,128是CPU训练的舒适区
epochs50~100看loss曲线收敛情况,一般50轮左右开始稳定
sequence_length100输入音符序列长度
LSTM units256隐藏层神经元数,越大表达力越强但越容易过拟合
dropout0.3防止过拟合

训练过程的目标是让categorical_crossentropy(多分类交叉熵)不断下降。每个epoch结束时,模型会学习“根据前100个音符,下一个音符的概率分布”。这里的分类数等于音符合表大小,通常几百到上千,取决于数据集里出现过的不同音符和和弦的种类数。

from keras.callbacks import ModelCheckpoint checkpoint = ModelCheckpoint( 'music_weights.hdf5', monitor='loss', verbose=1, save_best_only=True, mode='min' ) history = model.fit( x, y, batch_size=128, epochs=80, callbacks=[checkpoint] )

ModelCheckpoint在这里的作用是保留loss最低的那一次权重。训练中途如果过拟合了,你可以回头加载这个checkpoint而不是用最后一步的权重。monitor='loss'而不是'val_loss'是因为这个项目没有单独切验证集,全部数据都用于训练,这样在小数据集上能充分利用样本。

3.3 训练过程踩到的一个关键点:loss下降但生成结果不进步

这是一个非常典型的LSTM音乐生成现象:loss从2.5降到1.2左右时,生成结果开始有“像样”的趋势,但如果继续训练到loss降到0.2以下,生成结果反而变成同一段旋律的无限循环。原因很简单——模型把训练集中的某些曲子“背”下来了,它找到了最小化loss的捷径是精确复制片段。

我在跑这个项目时,遇到过loss已经很低但输出音符全是学过曲子的连续片段的情况。解决方式是:训练到loss下降趋势变缓时就停止,不要追求极限收敛。课程设计里展示生成结果时,选取中间epoch的权重往往比最后epoch的权重效果更好。

3.4 训练速度与硬件选择的实际体验

如果在笔记本电脑CPU上训练,数据量在数万序列、词表几百的条件下,每个epoch大约需要一到三分钟。80个epoch就是一到四小时,这个时间成本对课程设计场景是可以接受的。如果手头有支持CUDA的显卡,把同样的代码放到GPU上,一个epoch能压缩到十秒级别。

训练过程中的损失曲线,我一般会画出来放到说明文档里,这比说一百句“模型收敛良好”都有说服力。

4. 从训练到生成:Temperature采样与音乐续写实现

4.1 生成的核心逻辑:不是取最大值,而是按概率抽样

模型训练完成后,生成阶段要做的事情是:给定一段种子序列(比如训练集里随机截取的100个音符),预测下一个音符的概率分布,然后从分布中抽样。这里有一个极其关键的控制参数——temperature。

def sample_with_temperature(preds, temperature=1.0): """从预测概率分布中采样,temperature控制随机性""" preds = np.asarray(preds).astype('float64') preds = np.log(preds + 1e-8) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) probas = np.random.multinomial(1, preds, 1) return np.argmax(probas)

temperature的作用是重塑概率分布的尖锐程度。temperature越小(比如0.5),分布越尖锐,模型倾向选择高概率音符,结果更保守、更稳定,但也容易重复;temperature越大(比如1.5),分布越平坦,低概率音符也有机会被选中,结果更有“创造性”,但噪音也更多。这个项目默认值取1.0,实际调参时建议在0.6到1.2之间尝试。

np.random.multinomial按概率分布抽取一个样本,这样既不会每次都选最高概率造成呆板重复,也不会完全随机。注意在计算时加一个1e-8的极小值做平滑,防止log(0)报错——这是字符级生成任务里的经典细节。

4.2 生成歌曲的完整流程

生成阶段在主脚本create_music_py.py里被封装成了函数:加载训练好的权重,输入种子序列,逐音符预测并采样,每生成一个音符就把它接在序列末尾,同时丢掉最前面的一个音符,保持输入窗口长度恒定。

def generate_music(model, seed_sequence, num_notes=500, temperature=1.0): """从种子序列出发生成指定数量的新音符""" int_to_symbol = {idx: sym for sym, idx in symbol_to_int.items()} current_sequence = seed_sequence.copy() generated = [] for _ in range(num_notes): # 把当前序列转为one-hot x_input = np.zeros((1, len(current_sequence), vocab_size)) for i, idx in enumerate(current_sequence): x_input[0, i, idx] = 1 preds = model.predict(x_input, verbose=0)[0] next_idx = sample_with_temperature(preds, temperature) generated.append(int_to_symbol[next_idx]) current_sequence.append(next_idx) current_sequence = current_sequence[1:] return generated

逐音符生成是序列生成模型的标准推理方式。这里有一个性能点:每生成一个音符就调用一次model.predict,500个音符就是500次前向传播,CPU上可能耗时十几秒到一分钟,这是正常现象。如果想加速,可以用model.predict_on_batch减少调用开销。

4.3 把生成的音符序列转成MIDI可播放文件

生成的音符列表还是字符串,需要把它转回MIDI格式才能听到声音。项目里用music21的stream和note对象重建乐谱,然后输出为.mid文件。

from music21 import stream, note, chord, duration def notes_to_midi(generated_notes, output_file='output.mid'): """把音符字符串列表转成MIDI文件""" offset = 0 output_notes = [] for pattern in generated_notes: if '.' in pattern: # 和弦:拆开并同时播放 chord_notes = [note.Note(n) for n in pattern.split('.')] for n in chord_notes: n.duration = duration.Duration(0.5) chord_obj = chord.Chord(chord_notes) chord_obj.offset = offset output_notes.append(chord_obj) else: # 单音 n = note.Note(pattern) n.duration = duration.Duration(0.5) n.offset = offset output_notes.append(n) offset += 0.5 midi_stream = stream.Stream(output_notes) midi_stream.write('midi', fp=output_file)

每个音符的时值统一设为0.5秒,这是简化处理。真实音乐中音符时值变化是情感表达的重要部分,但作为课程设计,保持统一时值可以降低生成结果的杂乱程度。如果你希望更丰富的节奏,可以进一步训练一个同时预测音符和时值的模型,但这会让项目复杂度上升一个量级。项目的UI.ipynb里把生成功能包装成了带交互界面的版本,运行后可以直接在Notebook里输入参数、点击生成、播放结果,演示效果比纯命令行好很多。

5. LSTM音乐生成的常见问题与避坑记录:从loss不降到MIDI无法播放

5.1 训练时loss不降或下降极慢

现象:训练了好几个epoch,loss一直停留在2.5以上,基本没有下降趋势。

原因:最常见的是数据预处理环节出了问题,音符序列里混入了大量噪声符号,比如力度标记、时值标记被当作独立音符提取出来,导致词表非常大且分布极其稀疏。另一个可能原因是学习率不合适,rmsprop默认学习率在0.001左右,但如果特征分布异常,收敛会非常慢。

解决:先检查symbol_to_int映射表的大小,如果词表超过2000甚至3000,大概率是解析时把非音符元素也收进去了。回到parse_midi函数,过滤掉note.Note和chord.Chord之外的元素。另外可以打印训练集里前100个音符看看是否有异常符号。

5.2 生成的音乐反复重复同一个小片段

现象:生成的500个音符听上去像是某一段旋律在无限循环,变化极少。

原因:这是过拟合的典型表现,模型把训练数据背下来了。在数据集只有几十首曲子的情况下,训练轮数过多、dropout设置过低都会导致这个问题。

解决:把训练轮数减少到loss曲线刚开始走平的位置,或者增大dropout到0.4试试。另外可以调高temperature到1.2,让采样分布更平坦,减轻重复感。我通常会在生成阶段用0.8、1.0、1.2三个temperature各生成一段,对比后选效果最好的。

5.3 生成结果格式正确但MIDI文件无法播放

现象:程序正常输出output.mid,但用播放器打开后没有声音,或者提示文件损坏。

原因:midi_stream.write('midi', fp=...)这一步在music21版本较老时,有时会写出格式不兼容的文件。另一个可能是生成的音符中有不存在的音名,比如解析阶段产生的残留字符串在生成阶段被原样输出。

解决:在notes_to_midi里加一个校验,遇到无法被note.Note()解析的字符串就跳过并打印警告。同时确认music21版本,如果用的是老版本,升级到最新版通常能解决文件格式问题。

5.4 数据集太大导致预处理内存溢出

现象:处理多首MIDI时,内存占用飙升,进程卡死或直接被系统杀掉。

原因:create_sequences函数用双重循环把所有序列一次性生成并保存在内存里。数据集音符总量达到十万级别时,one-hot编码后的矩阵会非常庞大。以100个音符序列长度、500个音符合表为例,十万个训练样本就是10万×100×500的矩阵,如果用float类型,内存占用会达到几十GB。

解决:切分序列时使用生成器而不是一次性构建全部数组,或者把数据分批写入HDF5文件。我在处理类似项目时的做法是:先用一个小数据集验证流程,全量训练时把create_sequences改成逐步yield,配合model.fit的steps_per_epoch参数。

5.5 运行notebook时第三方库版本冲突

现象:import music21或import keras时直接报错,常见的是Aborted (core dumped)或者缺少DLL文件。

原因:music21依赖matplotlib和numpy,Keras 2.x和TensorFlow 2.x之间的版本匹配关系比较敏感。Python 3.7到3.9之间,某些组合会存在二进制不兼容问题。

解决:建议先创建虚拟环境,然后用pip install music21 keras tensorflow numpy一次性安装最新版本。如果你用的是Python 3.10及以上,优先选择TensorFlow 2.10以上版本。这个项目是在Keras 2.x时代写的,如果你的环境默认装了Keras 3,可以考虑把from keras...改为from tensorflow.keras...,兼容性更好。

6. 从生成到验证:最后一公里,怎么判断自动生成的音乐真的“有效”

生成只是一个中间节点,作为课程设计,你还需要向老师或者读者证明“这是有效的、有依据的”。我的做法是做一个三层验证。第一层:把生成的MIDI转成音频,听一遍,记录整体印象——它是否有旋律感、节奏是否稳定、有没有明显的突兀跳变。第二层:把训练集里的真实曲子和生成曲子的音符分布做一个对比,看音高分布的直方图是否接近,这说明模型学到了训练集的音高偏好。第三层:用音程分析做定量评估,统计生成序列中相邻音符之间的音程跨度,音乐理论认为二度到五度的级进和跳进是旋律的主要构成,如果生成结果频繁出现超过八度的跳跃,说明采样参数太激进了。

在参数层面,我会固定随机种子后对比不同temperature的输出长度分布和重复率,这个指标比主观听感更容易写进说明文档。我当时做完这个项目后的一个习惯是:每次训练完,一定用训练集里没有出现过的一首MIDI的前50个音符作为种子去生成,而不是自己手工构造种子序列——这样生成结果的差异性更能反映模型的泛化能力,而不是对特定片段的记忆。

如果你想要给答辩老师现场演示,最稳的路线是用UI.ipynb里的交互界面,先生成一小段(100到200个音符),当场播放,再切换temperature调出不同风格的两段对比。整个过程控制在三分钟以内,既不冷场也不会暴露生成质量不稳定的短板。我最开始做这个项目时犯过一个错,把temperature调成2.0生成了一段“天马行空”的音乐,答辩时播放出来效果非常杂乱,后来每次演示我都强制把temperature锁在0.8到1.2之间,生成前先跑一遍验证。希望这个教训能帮你少走一次弯路。

另外提醒一句,详细设计说明书PDF里包含的数据流图、模块划分和测试用例,是答辩时最有力的支撑材料。不要只展示代码,把预处理流程、模型结构图、loss曲线、生成示例这些内容按章节组织好,你的课程设计就完整了。希望这个项目能帮你顺利拿到高分,也希望你通过亲手复现,真正理解LSTM在序列生成任务上为什么有效、边界在哪里。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询