简介这份资源是面向深度学习与音乐生成方向学习者的实战代码包围绕在Midi数据集上训练神经网络生成音乐展开适合具备Python基础、希望入门序列生成与概率模型的中高级开发者。包内以Python脚本为核心涵盖贝叶斯超参数搜索、数据集预处理与numpy数组构建、模型训练与音乐生成等完整流程并借助TensorFlow、Keras、music21与pygame等库实现从数据到音频的闭环。资源共0个文件压缩包约1.02MB体量轻便便于快速部署与二次开发。目前已有187人学习关注可作为硕士论文《具有深度概率模型的音乐完成》的配套实践参考。读者可据此掌握Midi数据清洗、超参数调优、模型训练与生成评估的完整链路理解深度概率模型在音乐补全任务中的落地方式并在此基础上迁移到其他序列生成场景。1. 用 Keras 搭一个能写 MIDI 的深度神经网络从音符序列到可播放文件手里有一堆 MIDI 文件想让模型学着写出一段能听的旋律这件事比想象中直接。midiGenerator这个资源包干的就是这件事用 TensorFlow/Keras 搭一个深度神经网络把 MIDI 里的音符序列喂进去训练再让模型一个音一个音地预测出新的序列最后用 music21 写回标准 MIDI 文件配合 pygame 直接播放试听。整条链路是 Python 生态里最成熟的一套组合——music21 负责解析和生成乐谱结构Keras 负责建模pygame 负责让你不用装 DAW 就能听到结果。它适合已经会写基础 Python、想入门序列生成的人也适合做音乐信息检索方向、需要一个能跑通的最小闭环的从业者。下面按「数据怎么进、模型怎么搭、结果怎么出、坑在哪」的顺序拆一遍。2. 数据管线把 MIDI 拆成模型能吃的音符序列2.1 为什么选 music21 而不是自己解析二进制MIDI 文件本质是二进制事件流直接读字节当然可以但你会立刻撞上变长量、running status、多轨同步这些细节。music21 把这些封装成了Stream、Note、Chord对象取音高、时值、偏移量都是一行属性访问。常见做法是用converter.parse()把每个 MIDI 读成 Stream再flatten().notes拿到按时间排好的音符列表。这里有个选型理由值得说清music21 的解析速度不算快几千个文件可能要跑几分钟但它的正确性经过大量乐谱验证比手写解析器省下的调试时间多得多。如果你后续要处理几万首再考虑换mido做批量预处理但原型阶段没必要。2.2 提取音符与和弦统一成可编码的 token模型不认识Note对象得先把每个音符转成字符串 token。音高用pitch.nameWithOctave比如C4、F#5和弦用chord.normalOrder排序后拼起来休止符单独标记。这样每个时间步就是一个 token整首曲子变成一个 token 列表。from music21 import converter, note, chord import glob def extract_tokens(midi_dir): all_tokens [] for file in glob.glob(f{midi_dir}/*.mid): try: stream converter.parse(file) except Exception as e: # 损坏文件直接跳过别让一个坏样本中断整批 print(fskip {file}: {e}) continue tokens [] for element in stream.flatten().notes: if isinstance(element, note.Note): tokens.append(str(element.pitch.nameWithOctave)) elif isinstance(element, chord.Chord): # 和弦按音高排序后拼成 C4.E4.G4 形式 tokens.append(..join(str(n) for n in element.normalOrder)) if tokens: all_tokens.append(tokens) return all_tokens逻辑说明flatten()把多声部压成单序列notes只取音符和和弦自动过滤掉拍号、调号这些非演奏元素。参数上glob的路径按你实际存放 MIDI 的目录改normalOrder返回的是音级整数转成字符串是为了和单音 token 统一进同一个词表。注意异常捕获那一段——实际数据集里总有几个文件是坏的不跳过的话整批预处理会直接崩掉。2.3 建立词表并做序列切分把所有 token 收集起来去重得到词表。然后要决定用多长的窗口做输入。常见做法是取 50 到 100 个 token 作为一个样本前 n 个预测第 n1 个。窗口太短模型学不到乐句结构太长则训练慢且容易过拟合。我一般先用 64 试看验证集损失再调。from tensorflow.keras.utils import to_categorical import numpy as np def build_dataset(all_tokens, seq_len64): vocab sorted(set(t for seq in all_tokens for t in seq)) token_to_id {t: i for i, t in enumerate(vocab)} X, y [], [] for seq in all_tokens: ids [token_to_id[t] for t in seq] for i in range(len(ids) - seq_len): X.append(ids[i:i seq_len]) y.append(ids[i seq_len]) X np.array(X) y to_categorical(y, num_classeslen(vocab)) return X, y, vocab, token_to_id逻辑说明vocab排序是为了让每次运行词表顺序一致方便复现。to_categorical把标签转成 one-hot配合后面的 softmax 输出。参数seq_len就是前面说的窗口长度改它要同步改模型输入维度。这一步内存占用和样本数成正比如果 MIDI 很多X可能撑爆内存那就改用生成器分批喂。3. 模型搭建与训练LSTM 还是 GRU层数和维度怎么定3.1 网络结构的选择理由序列生成任务里LSTM 和 GRU 都是常见选择。GRU 参数少、训练快在中小规模数据上表现和 LSTM 差不多LSTM 在长依赖上略稳但门控多、显存吃得多。这个资源场景下数据量通常不大几百到几千首我倾向先用 GRU 跑通再换 LSTM 对比。结构上典型配置是Embedding 层把 token id 映射成稠密向量接一到两层循环层最后 Dense softmax 输出词表概率分布。Embedding 维度取 64 到 256循环单元数取 128 到 512这些不是玄学是显存和表达力的折中。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, GRU, Dense, Dropout def build_model(vocab_size, seq_len, emb_dim128, units256): model Sequential([ Embedding(vocab_size, emb_dim, input_lengthseq_len), GRU(units, return_sequencesTrue), Dropout(0.3), GRU(units), Dropout(0.3), Dense(vocab_size, activationsoftmax), ]) model.compile(losscategorical_crossentropy, optimizeradam, metrics[accuracy]) return model逻辑说明第一层 GRU 设return_sequencesTrue是为了把序列信息传给第二层第二层只返回最后一步的隐状态。Dropout(0.3)是防过拟合的常规手段数据越少越要加。损失用交叉熵是因为每个时间步本质是多分类。参数emb_dim和units是主要调参对象显存不够就先降units。3.2 训练过程与检查点训练时把数据切一部分做验证集用ModelCheckpoint保存验证损失最低的权重。批次大小常见取 64 或 128轮数看损失曲线通常几十轮就收敛。这里有个血泪经验不要只看训练准确率序列生成任务里训练损失降得很低但生成结果全是重复音符是常事必须定期用模型实际生成一段来听。from tensorflow.keras.callbacks import ModelCheckpoint checkpoint ModelCheckpoint(best.keras, monitorval_loss, save_best_onlyTrue, verbose1) model.fit(X, y, batch_size128, epochs80, validation_split0.1, callbacks[checkpoint])逻辑说明validation_split0.1从训练数据里划 10% 做验证数据量大时可以改成显式传入验证集。save_best_onlyTrue保证只留最好的那一版避免最后一轮过拟合的权重覆盖掉好模型。注意 Keras 新版本保存格式是.keras老代码里的.h5也能用但会有告警。3.3 生成时的温度参数生成不是取概率最大的那个 token 就完事那样结果会很单调。常见做法是引入温度温度趋近 0 时接近贪心趋近 1 时按原始分布采样大于 1 会更随机但也更容易跑调。我一般从 0.8 到 1.0 之间试想要稳定旋律就调低想要意外惊喜就调高。def sample_with_temperature(preds, temperature1.0): 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) return np.random.choice(len(preds), ppreds)逻辑说明先取对数再除以温度等价于对原始概率做幂次缩放加1e-8是防止 log(0)。这个函数每次只采一个 token生成整段时循环调用把新 token 接到序列尾部再喂回模型。4. 从 token 回到 MIDImusic21 写文件与 pygame 试听4.1 把预测序列还原成 Stream生成出来是一串 token得反向映射回音符对象。单音 token 直接建note.Note带点的按和弦处理。每个音符给一个固定时值常见是八分音符或四分音符偏移量按顺序累加。from music21 import stream, note, chord def tokens_to_stream(tokens, note_dur0.5): output stream.Stream() offset 0.0 for t in tokens: if . in t: # 和弦 tokennormalOrder 是音级整数转回音名 pitches [str(p) for p in t.split(.)] c chord.Chord(pitches) c.offset offset output.insert(offset, c) else: n note.Note(t) n.offset offset output.insert(offset, n) offset note_dur return output逻辑说明note_dur控制每个音符占多少拍0.5 对应八分音符。和弦那段的音级整数转音名在 music21 里有现成方法这里简化成字符串拼接实际用的时候建议查一下pitch.Pitch的转换接口避免调号问题。insert的 offset 必须单调递增否则 music21 会报错。4.2 写出 MIDI 并用 pygame 播放写文件一行搞定播放则需要 pygame 的 mixer。注意 pygame 播放 MIDI 依赖系统里的音源不同平台表现不一样这是最容易翻车的地方。import pygame def play_midi(path): pygame.mixer.init() pygame.mixer.music.load(path) pygame.mixer.music.play() while pygame.mixer.music.get_busy(): pygame.time.wait(100) output_stream tokens_to_stream(generated_tokens) output_stream.write(midi, fpgenerated.mid) play_midi(generated.mid)逻辑说明write(midi, fp...)是 music21 的标准写法第一个参数是格式。get_busy()轮询保证播放期间程序不退出。如果播放没声音先确认系统有没有 MIDI 音源Linux 上常见要装 soundfont这是环境问题不是代码问题。5. 避坑与排查训练不收敛、生成全是重复音、播放没声音5.1 损失不下降或震荡现象训练几轮后 loss 卡住不动或者上下大幅震荡。原因通常是学习率太大、批次太小或者数据里 token 分布极度不均某个音出现频率过高。解决先把学习率降到 1e-3 以下试批次调到 128再统计词表频率对高频 token 做下采样或给损失加权重。还有一种情况是序列窗口太长导致梯度问题把seq_len从 64 降到 32 往往能救回来。5.2 生成的旋律全是同一个音现象模型输出一长串相同的 token听起来像卡带。原因一般是训练不充分或者生成时温度太低走了贪心。解决先确认训练轮数够不够验证损失是否还在降再把温度调到 0.9 到 1.0 之间。如果还是重复检查训练数据本身是不是就有大量重复片段——数据质量决定生成质量这点没有后悔药。5.3 和弦 token 还原时报音高错误现象tokens_to_stream处理带点的 token 时抛异常或生成的和弦音不对。原因是normalOrder返回的是音级整数0 到 11直接当音名用会错。解决用 music21 的pitch.Pitch(midi...)或先转成chromatic再取 name别用字符串硬拼。这个坑在第一次跑通单音、加和弦时几乎必踩。5.4 pygame 播放无声或报错现象mixer.music.load成功但没声音或者直接抛pygame.error。原因是系统缺少 MIDI 音源或 mixer 初始化参数不对。解决先单独用系统播放器打开生成的.mid确认文件本身没问题再检查 pygame 版本和音频后端。实在不行就跳过播放用 DAW 或在线 MIDI 播放器试听别在环境上耗太久。5.5 显存溢出现象训练到一半报 OOM。原因是X一次性全载入内存或者units、batch_size太大。解决改用tf.data.Dataset做流式加载把units从 512 降到 256批次从 128 降到 64。序列任务里显存和序列长度是平方关系降seq_len比降units更有效。6. 进阶技巧用多轨生成和种子序列控制输出风格跑通单轨之后想让生成结果更可控有两个方向值得试。一是种子序列不要从随机 token 开始而是拿一段真实 MIDI 的前几十个 token 当输入让模型接着往下写这样风格会贴近种子。二是多轨把不同乐器轨分别提取、分别训练生成时按轨拼接能得到有伴奏的片段。种子序列的实现很简单把X的初始窗口换成种子 token 的 id 列表即可注意长度必须等于seq_len。参数作用常用取值调整方向seq_len输入窗口长度32 / 64 / 100短则快但结构弱长则慢且吃显存emb_dimtoken 嵌入维度64 / 128 / 256词表大就调大units循环单元数128 / 256 / 512显存不够先降这个temperature采样温度0.8 / 1.0 / 1.2低则稳高则野note_dur输出音符时值0.25 / 0.5 / 1.0控制节奏密度验证生成质量别只靠耳朵。我一般会统计生成序列的 token 分布和训练集的 KL 散度散度太大说明模型没学到数据分布再看平均音程跨度跨度过大通常意味着跑调。这两个指标比单纯听更客观也方便调参时对比。从那以后我每次改完模型结构都强制先用种子序列生成 30 秒听一遍再决定要不要继续训练——耳朵比损失曲线诚实。希望帮到你。本文还有配套的精品资源点击获取