简介:这份资源围绕深度学习驱动的音乐生成设计完整实践教程,面向AI开发者、算法学习者及音乐科技爱好者。内容以歌词为输入生成旋律,再以旋律为输入生成伴奏,两个模型可分别训练,覆盖自然语言处理、音乐特征建模与序列生成等关键环节。资源共85个文件,包含22个Python脚本、示例样本、MIDI/歌词数据、Jupyter Notebook、Markdown说明及若干配置文件,压缩包约226MB,目录按training、inference、evaluation等模块组织,便于按阶段对照学习。目前已有409人学习浏览,适合用于环境搭建、数据预处理、模型训练及音乐生成API部署的完整复现。通过实践可掌握LSTM、Transformer、VAE等模型在歌词-旋律-伴奏任务中的具体用法,了解数据增强、损失函数定义与模型保存部署等工程细节,适合希望从零构建音乐生成系统并提升AI应用能力的读者。
1. 歌词生成旋律与旋律生成伴奏:为什么拆成两个模型而不是端到端
我想写一首歌,手头只有几句歌词,先让程序根据歌词生成旋律,再用旋律生成伴奏,这是很多音乐 AI 入坑者都会有的念头。但真把数据摆到桌面上,你会发现歌词、旋律、伴奏三者的对应关系高度不确定,同一句歌词给十个作曲家能谱出十种完全不同风格。如果强行做一个端到端的“歌词进、伴奏出”大模型,你既要解决文本到长序列对齐,又要让和声、节奏、配器同时收敛,数据量、算力、评估方式全都会变成无底洞。所以我的落地做法,就是把任务拆成两个独立训练的深度学习模型:第一个模型以歌词为输入生成旋律,第二个模型以旋律为输入生成伴奏。拆开之后,每个模型的数据都能单独构造,loss 也能单独观察,你随时能听到中间产物,才敢往下走。
这篇笔记适合已经有模型训练经验、但第一次碰音乐序列生成的人,也适合想用 MIDI 数据做编曲辅助的算法工程师。下面我会从环境搭建开始,把两个模型的实现、训练参数、数据格式以及我实际踩过的坑全部写清楚。
2. 搭建可复现的深度学习训练环境:依赖版本和一条命令跑通
训练环境是整个项目第一个坑,也是大多数“代码能跑但训练失败”的根源。音乐序列生成和普通 CV 项目不太一样,它同时依赖深度学习框架、MIDI 解析库和音频处理库,这三者经常因为 Python 版本或 numpy 版本打架。我建议先定死一套组合,再在里面安装东西,不要用到哪装到哪。
2.1 版本选择:Python 3.10、PyTorch 2.1、CUDA 11.8
我常用的组合是 Python 3.10.13、PyTorch 2.1.2、CUDA 11.8,配套 torchaudio 2.1.2。为什么不用最新版本?因为音乐库生态往往比视觉库慢半拍。比如 numba 和 llvmlite 对 Python 3.12 的支持曾经很滞后,而 librosa 又依赖 numba,装上去就会出现各种“玄学”编译错误。CUDA 11.8 的兼容性也很广,从 RTX 3090、4080 到云上的 T4、A10 都能直接跑,驱动门槛比 CUDA 12 低。
| 组件 | 版本 | 理由 |
|---|---|---|
| Python | 3.10.13 | numba、llvmlite 支持稳定 |
| PyTorch | 2.1.2 | 与 cu118 配套,可启用 torch.compile |
| CUDA Toolkit | 11.8 | 兼容新旧显卡,驱动要求低 |
| music21 | 8.1 | MIDI 解析和乐理工具 |
| pretty_midi | 0.2.10 | 轻量读取 MIDI 事件 |
还有一点容易被忽略:不要用系统 Python,也不要 sudo pip 全局安装。深度学习环境至少要能一键重建,conda 隔离是成本最低的办法。
2.2 用 Conda 创建防污染环境的安装步骤
下面的命令会创建一个名为 py310 的环境,并把训练所需的核心库全部装好。这段流程我每次在新机器上都会先跑一遍,顺便验证 CUDA 是否真的可用。
# 创建深度学习专用环境,py310 是环境名 conda create -n py310 python=3.10.13 -y conda activate py310 # 安装核心深度学习框架,--index-url 指定 cu118 渠道 pip install torch==2.1.2 torchaudio==2.1.2 --index-url https://download.pytorch.org/whl/cu118 # 安装 MIDI 解析和训练辅助库 pip install music21 pretty_midi numpy pandas tensorboard为什么先 conda 再 pip,而不是直接用 pip 装到基础环境?因为 music21 和 pretty_midi 都依赖 numpy,如果基础环境里已经有别的项目装过 numpy 1.x,两个库可能链接到错误版本。conda 环境隔离后,pip 装的依赖都会落在 py310 这个单独环境里,不会污染其他项目。
--index-url这段参数很关键。PyTorch 官方把 CPU 版和 CUDA 版放在不同的 whl 渠道里,默认 PyPI 源装到的通常是 CPU 版本,只有指定 cu118 才能拿到带 CUDA 算子的版本。如果你的机器没有 NVIDIA GPU,可以把 cu118 换成 cpu,但训练速度会差一个数量级,后面做序列生成会非常痛苦。
2.3 验证环境:MIDI 工具链和 CUDA 是否真的可用
环境装完别急着训练,先跑一个最小验证。这个验证能同时确认深度学习框架、MIDI 写读、以及音频事件解析链路是否通畅。
# check_env.py import torch, music21, pretty_midi print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) # 生成一个 C 和弦 MIDI 文件做测试 from music21 import stream, chord s = stream.Stream() s.append(chord.Chord(["C4", "E4", "G4"])) s.write("midi", fp="test.mid") pm = pretty_midi.PrettyMIDI("test.mid") piano_roll = pm.get_piano_roll(fs=16) print("piano roll shape:", piano_roll.shape)逻辑说明:torch.cuda.is_available()输出 True 只说明框架能看到驱动,不一定代码能用到;更可靠的方式是后续训练时观察显卡显存占用。music21 写出 test.mid,pretty_midi 再读回来,这一步验证了 MIDI 文件从写入到解析的事件流是通的。如果这里失败,后面所有数据处理都会在加载阶段秒挂。
参数说明:get_piano_roll(fs=16)返回一个 (128, 时间帧) 的矩阵,行是音高 0-127,列是按每秒 16 帧切出来的时间步。这个 fs 不是采样率,而是时间分辨率,fs 越大,音长越精确,但序列越长,我通常用 16,后面生成模型里时值 token 的粒度也和它一致。
3. 歌词生成旋律:从文本特征到音符序列的 Seq2Seq 实现
歌词生成旋律本质上是一个条件文本生成任务:输入歌词的字符序列,输出一个离散的音符序列。这个任务最稳妥的模型结构是 Seq2Seq,而不是直接套一个大语言模型。因为数据量通常只有几千到几万对,大规模预训练模型很容易过拟合,而且解码时无法控制音长和 melody 的跨度。我自己实现的基线是双向 LSTM 编码器加带注意力的 LSTM 解码器,输出端拆成音高和时值两个分类头。
3.1 歌词-旋律数据集:从 MIDI 到 token 序列的对齐
数据组织是整个任务成败的一半。我从开源 MIDI 库里筛出有人声主旋律的曲子,再把歌词按句子切分,旋律也按乐句切分。这里有一个容易被忽略的准则:不是每个字对应一个音,而是按句边界对齐。所以每个训练样本是“一句歌词 + 同一时间窗口内的主旋律音符序列”。
下面这个函数把一首 MIDI 的主旋律转成符号序列,时间精度用刚才环境里的 fs=16。
# parse_midi.py:把 MIDI 主旋律转成 [pitch, duration] token import pretty_midi def extract_melody(midi_path, fs=16): pm = pretty_midi.PrettyMIDI(midi_path) # 假设主旋律在第一个乐器音轨,通常与人声轨一致 inst = pm.instruments[0] notes = [] for n in inst.notes: pitch = n.pitch # 0-127 duration = int(round((n.end - n.start) * fs)) # 过滤掉极端低音和极端高音,减少无效类别 if 48 <= pitch <= 84: notes.append((pitch, duration)) return notes逻辑说明:这里的简单前提是主旋律在第一个非打击乐器轨,如果你的 MIDI 文件排序不固定,需要事先做音轨分类,否则会把鼓或者贝斯当成旋律。过滤音高范围到 48-84,也就是 C3 到 C5,这是人声主旋律最常出现的音区,超出部分可以整体八度移调之后再进模型。
时长 token 的粒度直接决定生成节奏。fs=16 时,一个四分音符等于 4 个时间帧,八分音符等于 2 帧,十六分音符等于 1 帧。解码器输出 32 类时长 token,最大支持 2 秒时值。不要让模型输出浮点秒数,分类比回归稳定得多。
歌词这边我采用字符级切分,而不是词级。中文歌词本来就没有严格分词边界,用字符级可以避免词表爆炸,也让模型能处理没见过的字。如果你的歌词是英文,可以考虑用 BPE;不要一上来就拿 Longformer 中文模型编码,因为小数据集下字符级 LSTM 的效果不见得差,而且处理速度会快很多。
3.2 Seq2Seq 模型:字符级歌词编码与注意力解码
选 LSTM 而不是 Transformer,核心理由是数据量。歌词到旋律的映射没有公开的超大配对语料,Transformer 在小数据下学习位置偏置和注意力模式都比较吃力。LSTM 加注意力是序列生成里最成熟、最容易调试的基线。
下面是模型的核心结构。编码器是双向 LSTM,输出每个位置的隐状态;解码器是单向 LSTM,每步通过注意力从编码器状态中抽取与当前音符相关的歌词上下文。
# model.py:歌词 -> 旋律 的 Seq2Seq 模型 import torch import torch.nn as nn import torch.nn.functional as F class Encoder(nn.Module): def __init__(self, vocab_size, emb_size, hidden_size): super().__init__() self.emb = nn.Embedding(vocab_size, emb_size) self.lstm = nn.LSTM(emb_size, hidden_size, bidirectional=True, batch_first=True) def forward(self, x): emb = self.emb(x) out, (h, c) = self.lstm(emb) return out, h, c # out 用于注意力 class Decoder(nn.Module): def __init__(self, vocab_size, emb_size, hidden_size): super().__init__() self.emb = nn.Embedding(vocab_size, emb_size) self.lstm = nn.LSTM(emb_size, hidden_size, batch_first=True) # 注意力打分:拼接编码器隐层和解码器隐层 self.attn = nn.Linear(hidden_size * 2 + hidden_size, 1) self.pitch_head = nn.Linear(hidden_size * 2, 128) self.duration_head = nn.Linear(hidden_size * 2, 32) def forward(self, x, encoder_outs, state): emb = self.emb(x) out, state = self.lstm(emb, state) # 当前解码时刻最后一个时间步的隐层 src_len = encoder_outs.size(1) h_t = out[:, -1, :].unsqueeze(1).repeat(1, src_len, 1) attn_input = torch.cat([encoder_outs, h_t], dim=-1) score = self.attn(attn_input).squeeze(-1) attn_w = F.softmax(score, dim=-1).unsqueeze(1) # 注意力加权得到上下文向量 context = torch.bmm(attn_w, encoder_outs) combined = torch.cat([context, out], dim=-1) pitch = self.pitch_head(combined) duration = self.duration_head(combined) return pitch, duration, state逻辑说明:attention 的打分函数是一个线性层,输入是双向编码器隐层(hidden_size * 2)和当前解码器隐层(hidden_size)的拼接,输出一个标量分数,softmax 后得到每个歌词位置对当前音符的权重。然后把权重作用在编码器隐层上,得到 context,再与解码器当前隐层拼接,送入两个分类头。
参数说明:hidden_size 我取 256,emb_size 取 128。这里的 hidden_size * 2 是因为编码器是双向的,前向和后向向量叠在一起;如果你换成单向编码器,对应的维度也要同步改。pitch_head 输出 128 维,正好对 MIDI 音高 0-127;duration_head 输出 32 维,对应最大 2 秒的整数帧时值。
3.3 训练脚本与超参数:teacher forcing 和掩盖填充
训练时最容易被忽略的是 mask。因为旋律序列有长有短,DataLoader 里要 pad 到同一长度,如果不把 pad 位置在 loss 里忽略掉,模型会发现“提前结束句子”是最低代价的偷懒方式,训练出来的旋律全是短句。
下面这段训练循环是我经常用的片段。其中 BOS_ID 是旋律序列的开始符,和歌词的结束符 对应。
# train_melody.py 核心训练循环(片段) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss(ignore_index=-100) for batch_x, pitch_y, dur_y in dataloader: optimizer.zero_grad() encoder_out, h, c = model.encoder(batch_x) state = (h, c) # 用 BOS 作为解码器第一步输入 decoder_input = torch.full((batch_x.size(0), 1), BOS_ID, dtype=torch.long) total_loss = 0 for t in range(pitch_y.size(1)): pitch_logits, dur_logits, state = model.decoder( decoder_input, encoder_out, state) loss_p = loss_fn(pitch_logits.view(-1, 128), pitch_y[:, t]) loss_d = loss_fn(dur_logits.view(-1, 32), dur_y[:, t]) total_loss += loss_p + loss_d # teacher forcing: 80% 概率用真实 token,否则用自己的预测 if torch.rand(1).item() < 0.8: teacher_token = pitch_y[:, t].unsqueeze(1) else: teacher_token = pitch_logits.argmax(-1) decoder_input = teacher_token total_loss.backward() optimizer.step()逻辑说明:这个循环是逐步解码的,每一步拿上一步的真实音高或预测音高作为输入,状态 state 在每一步之间不断更新。attention 的 context 是在模型内部计算的,这里不需要显式传入。loss 是音高交叉熵和时值交叉熵直接相加,我暂时不配权重,因为二者都在 0-1 之间。
参数说明:学习率 1e-3 配 AdamW,batch size 取 32。如果显存不够,可以把最大旋律长度限制在 200 个 token 以内,不要硬吃长序列。teacher forcing 比例 0.8 算是一个通用起点,太高会导致推理时一步错步步错,太低则收敛极慢。另外,如果你的数据集里旋律调性杂乱,生成时会飘,我建议先做一步移调到 C 大调。这件事我会在第 5 章再展开。
4. 旋律生成伴奏:把旋律当作条件输入,预测和弦和伴奏音型
旋律生成伴奏要比歌词生成旋律更“结构化”。如果让模型直接输出整条伴奏音符流,很容易生成一串没有和声逻辑的噪音。常见做法是先让模型根据旋律预测和弦进程,再根据和弦和旋律生成音型。这两个步骤可以都做成神经模型,也可以只有第一步用深度学习,第二步用规则去填充。下面的实现以和弦预测为核心,因为它直接决定伴奏好不好听。
4.1 伴奏建模:先抽取和弦,再生成音型
我先把伴奏任务简化成“每小节一个和弦 token”。为什么是小节?因为和声节奏通常以小节为单位变化,这样输出序列比逐音符生成短很多,模型也能更稳定。和弦 token 的定义我使用“根音 + 性质”,例如 C、Dm、Em、F、G、Am、Bdim,这样总共 24 类,也就是 12 个根音乘大小调。
为了让训练数据可获取,需要先从旋律里打标签。这里用一个简单的规则基准:统计小节内音高模 12 的直方图,出现最多的音级作为根音,再看有没有对应的大三度或小三度,决定性质。
# build_chord.py:从旋律音符统计音高,用规则抽候选和弦 from collections import Counter def estimate_chord(notes): # notes 是小节内音高列表,取 mod 12 统计音级 degrees = [p % 12 for p in notes] cnt = Counter(degrees) root = cnt.most_common(1)[0][0] # 如果根音上方大三度在小节内显著存在,判为大调 if (root + 4) % 12 in cnt: return root, "maj" if (root + 3) % 12 in cnt: return root, "min" return root, "maj"逻辑说明:这是启发式做法,不需要很准,目的是给训练数据打弱标签。模型会在训练中学习到比这个规则更平滑的和弦走势。如果你的数据集本身有标注和弦,完全可以直接替换成人工标注。
参数说明:只看音高 mod 12 会忽略低音和转位,比如 C 和弦第一转位里根音 C 不在最低音,但音级分布还是有。对于入门模型,这种现象不影响大局,后续可以加贝斯音轨来消歧。注意,这个函数输出的 root 是 0-11 的整数,需要再映射到和弦 token ID。
4.2 条件 Transformer:旋律 token 到和弦 token
有了和弦 token,模型结构就比较常规了。我把旋律的每个音符编码成“pitch * 32 + duration”的一个整数,作为 Transformer 编码器的输入,解码器输出的每个位置对应一个小节的和弦类别。用 Transformer 而不是 LSTM,是因为旋律和和弦之间是长距离对应关系,Transformer 的全局注意力能更好地抓住乐句骨架。
# accompaniment_model.py:旋律 -> 和弦 的条件 Transformer import torch import torch.nn as nn import math class Melody2ChordTransformer(nn.Module): def __init__(self, melody_vocab=4096, chord_vocab=24, d_model=128, nhead=4, num_layers=3): super().__init__() self.embed = nn.Embedding(melody_vocab, d_model) self.pos = PositionalEncoding(d_model) self.transformer = nn.Transformer( d_model=d_model, nhead=nhead, num_encoder_layers=num_layers, num_decoder_layers=num_layers, batch_first=True, ) self.out = nn.Linear(d_model, chord_vocab) def forward(self, melody_tokens, tgt_tokens): src = self.pos(self.embed(melody_tokens)) tgt = self.pos(self.embed(tgt_tokens)) out = self.transformer(src, tgt) return self.out(out) class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=512): super().__init__() pe = torch.zeros(max_len, d_model) pos = torch.arange(0, max_len).unsqueeze(1).float() div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(pos * div) pe[:, 1::2] = torch.cos(pos * div) self.register_buffer("pe", pe) def forward(self, x): return x + self.pe[: x.size(1)]逻辑说明:melody_vocab 我设成 4096,是因为 pitch 范围 128,duration 范围 32,二者相乘刚好是 4096。这种合并方式能在嵌入层之前保留“音高变化”和“时值变化”的双重信息。Transformer 的解码器输入是右移后的和弦 token 序列,推理时则用上一小节预测出的和弦作为下一小节的输入。关键点:编码器输入是整段旋律,解码器输入是逐步产生的和弦,位置编码用标准三角函数。
参数说明:d_model=128,nhead=4,layer=3 是一个适合小额数据的小配置。不要一上来就上 6 层,旋律-伴奏数据量通常只有几千首曲子,加大模型只会让过拟合提前到来。如果你用的是序列长度超过 512 的密集音符,请把 max_len 同步调大,否则位置编码会截断。
4.3 训练伴奏模型的损失和采样
训练目标是对每个小节的和弦类别做交叉熵分类。伴奏数据里主和弦占比很高,C、G、Am 出现频率远超 F 和 Dm,所以我会在 loss 里传一个类别权重,避免模型直接输出高频和弦。推理时不要 argmax,应该用带温度系数的采样,这样生成结果不会一直原地打转。
# train_accomp.py 训练伴奏模型的关键配置 from torch.optim.lr_scheduler import ReduceLROnPlateau config = { "batch_size": 64, "lr": 5e-4, "epochs": 30, "weight_decay": 0.01, "warmup_steps": 100, "temperature": 0.8, "class_weight": [1.0] * 24, # 根据训练集统计后填充 }逻辑说明:warmup_steps 对 Transformer 比较关键,前 100 步学习率从 0 线性升到设定值,可以避免早期更新太快把嵌入层学坏。temperature=0.8 的意思是采样前把 softmax logits 除以 0.8,让概率分布比原始分布略尖锐一点,减少随机和弦造成的突兀感。class_weight 我通常先训练两个 epoch,统计每个和弦出现的频次,然后用 median frequency balancing 来填充,而不是简单写死。
参数说明:batch_size 64 适合小块显存,30 epoch 对这个任务通常足够,再往后基本在拟合噪声。这里用的 ReduceLROnPlateau 会在验证 loss 连续 3 个 epoch 不降时把学习率乘 0.5,比固定学习率高很多。
5. 两个模型分别训练的常见问题排查:从数据到渲染的五处坑
分开训练最大的好处是问题可以逐个定位。但正因为分开,两个模型各自的输出边界很容易出问题。这一章我写五条我实际见过的坑,每条按现象、原因、解决三个层次展开。
5.1 现象:歌词被模型逐字复读,旋律完全没有乐句感觉
生成出来的旋律在每个歌词字下面都落一个音符,音高跟着字符顺序跑,节奏和断句完全没逻辑。原因一般是歌词和旋律在数据准备阶段没有按乐句对齐,模型学到的是“字到音”的一一映射,而不是“句到句”的对应。解决方法是把训练样本切到乐句级:歌词按逗号、句号、问号切分,旋律按休止符或长音切分;然后在解码器输入开头加一个 BOS,在歌词末尾加<sep>token,强制模型先读完整个乐句再开始生成。我还会在旋律数据里给每个音符序列后面加一个 rest token,让模型知道句子边界。
5.2 现象:loss 一直在降,但生成旋律从头到尾只有一个音高
这种情况通常伴随着 loss 漂亮但生成结果完全不可听。原因有两个:一是数据集音高分布太集中,比如大多数歌都在 C 大调主音附近;二是模型过于自信,softmax 输出接近 one-hot,解码时一旦选中一个音就往死里走。解决方法是给 pitch 的交叉熵加 label smoothing,我常用 0.1,让概率分布不要那么极端。另外,在数据预处理时对旋律做移调,把所有歌统一到 C 大调或 A 小调,但保留相对音程变化。这样模型学的是音程关系,而不是某个绝对音高。千万不要忘记这一点,很多类似项目都是栽在绝对音高与调性不统一上。
5.3 现象:伴奏模型生成的和弦永远重复 C 和 Am
如果你发现推理结果里高频和弦占比明显超过训练集分布,一般是两个原因:类别不平衡没有被 loss 权重修正,以及旋律输入的 token 编码里 duration 部分占的 bit 太大。我一开始用pitch * 32 + duration编码,发现模型几乎无视音高变化,只按时值节奏判断和弦,因为 duration 在索引里的变化更频繁。解决方法是把合并编码改成pitch * 128 + duration,让音高的变化对 token 索引的影响更大,这样谐波特征才能进入分类器。同时给 loss 加中位数频率权重,Dm、Em、Bdim 这些稀有类别会被放大。
5.4 现象:生成的 MIDI 文件在播放器里没有声音
这可能是最让人崩溃的“假训练失败”。MIDI 文件本身没问题,但播放器没有正确映射音色。原因通常是合成时没有指定 Program,默认音色可能落在打击乐通道,或者通道号是 10,而通道 10 在 GM 标准里固定是打击乐。解决方法是写文件前显式设置乐器程序号和通道。在 pretty_midi 里可以设置inst.program = 0代表 Acoustic Grand Piano,并把通道inst.channel = 0。在 music21 里则要设置 note.volume 和 music21 的 MIDI 程序属性。这个问题和环境无关,但会浪费你半天时间去查训练效果。
5.5 现象:两个模型单独生成都很正常,拼在一起调性和节奏对不上
这是把两个模型接起来后的典型问题。歌词生成旋律模型如果输出落在任意调,第二个模型训练时假设旋律主要在 C 大调或 A 小调,那结果必然冲突。解决方法是统一数据流水线:在训练歌词到旋律模型之前,对所有原始旋律做 key 检测并移调到 C 大调;推理时也要先对模型输出的旋律做 key 检测和移调,再喂给旋律到伴奏模型。节奏方面,歌词到旋律模型的时长 token 粒度和伴奏模型的小节边界必须一致。我一般统一用 fs=16,每 4 帧为一拍,每 16 帧为一小节,这样两个模型衔接时不会出现半小节错位。
6. 让生成结果能听:后处理与调参验证
模型训练完,别急着看 loss,先把生成结果用 MIDI 渲染出来听一遍。听感是你唯一的验证方式,数值指标只能帮你排除明显故障。我每次训练完都会写一个小脚本,把旋律和伴奏合到一个 MIDI 文件里,设定速度和力度,快速过一遍耳朵。
下面这段代码把音符 token 序列转成 music21 的 stream,并把时值从帧数转换为四分音符长度。
# render_and_check.py:把 token 序列渲染成 MIDI 试听 from music21 import stream, tempo, note s = stream.Stream() s.append(tempo.MetronomeMark(number=90)) for pitch, dur_frames in melody_tokens: n = note.Note(pitch) n.quarterLength = dur_frames / 4.0 # 4 帧等于一个四分音符 n.velocity = 96 s.append(n) s.write("midi", fp="demo.mid")逻辑说明:fs=16 时一拍等于 4 帧,因此dur_frames / 4.0得到四分音符长度。速度设为 90 BPM,力度 96 是一个适中的中强音。这样生成的 MIDI 文件就能在任意播放器里直接打开听。如果你加上伴奏轨,记得在轨道开头设置 Program 为钢琴。
参数调优方面,我习惯每次训练完生成 10 个样本,统计音高直方图和和弦重复率。音高直方图如果出现明显的单峰,说明模型坍缩;和弦重复率超过 0.5,说明伴奏模型没有学到变化。这两个指标比 loss 更能反映听感。模型结构上不需要反复调 attention 头数,先把数据一致性和解码采样温度解决,效果会有质的提升。
最后说一个我的血泪经验:不要一开始就追求“完整编曲”,先让模型生成 8 小节、只用一个音色的伴奏,听感和调试速度都更可控;等它稳定了,再加鼓、贝斯和副歌段落。这个方向上,稳定可听的短样本比偶发好听的完整长曲更有价值。希望这个拆分成两个模型分别训练的路线能帮你也少走一段弯路。
本文还有配套的精品资源,点击获取