简介本资源是一套基于LSTM模型的自动音乐生成实践项目面向机器学习初学者与音频AI兴趣开发者聚焦解决当前主流Simple RNN及WaveNet生成音乐同质化严重、听感生硬的问题。项目通过构建轻量级LSTM网络实现短曲自动生成与播放在降低人工干预前提下提升旋律连贯性与表现力适用于课程设计、算法对比实验或创意编程拓展。压缩包共8个文件466KB含2个核心Jupyter Notebookmain.ipynb负责模型训练与生成UI.ipynb提供简易交互界面、1个Python主程序create_music_py.py、2份Markdown文档含项目简介与README、2份LICENSE及1份PDF详细设计说明书结构清晰、模块分工明确。已有239人学习下载读者可直接复现完整流程从数据预处理、LSTM建模、MIDI生成到音频播放同时获得可调试代码、设计逻辑说明与合规开源许可具备较强的教学参考与二次开发价值。1. 为什么用机器学习生成音乐却总弹不出“人味儿”——这不是调参问题是建模逻辑断层你训练了一个 LSTM 或 Transformer 模型喂了 5000 首 MIDIloss 降到 0.02生成的音符序列语法正确、节拍对齐、和弦不打架——但一播放就像钢琴家戴着橡胶手套在敲键精准、干净、毫无呼吸感。这不是玄学是自动音乐生成Automatic Music Generation, AMG里最常被忽略的底层断层机器学习模型学的是符号映射不是音乐语义它优化的是局部概率不是听觉连贯性。本篇讲的“基于机器学习的自动音乐生成优化”不是教你换个更大模型或加更多数据而是从数据表征、损失函数设计、解码策略到人工干预闭环把“能生成”变成“值得听”。适合已跑通基础 pipeline比如用create_music_py.py生成过单声部旋律、但卡在质量瓶颈的工程师和作曲向开发者——尤其当你发现模型反复生成相似动机、转调生硬、动态起伏像心电图平直时这篇就是你的血泪排查清单。核心不在于“怎么让模型更聪明”而在于“怎么让它知道什么叫‘好听’”。2. 数据不是越多越好MIDI 预处理的三个致命陷阱与重编码方案自动音乐生成的质量天花板80% 取决于输入数据的结构合理性。很多项目直接用 raw MIDI 文件喂模型结果模型学了一堆“演奏噪音”踏板延音残留、音符重叠冲突、量化误差导致的微节奏抖动。下面这三步预处理是我在线上 7 个开源 AMG 项目包括main.ipynb中常见流程中复现后必须重做的关键动作。2.1 剥离演奏层只保留“作曲意图”丢掉“演奏痕迹”原始 MIDI 包含大量演奏信息velocity、pedal、pitch bend这些对生成旋律/和声无益反而干扰模型学习音高-节奏关系。正确做法是提取Note-On/Note-Off 事件流并做标准化import pretty_midi import numpy as np def clean_midi_to_notes(midi_path: str) - list: pm pretty_midi.PrettyMIDI(midi_path) notes [] for instrument in pm.instruments: if not instrument.is_drum: # 忽略鼓组单独建模 for note in instrument.notes: # 关键统一量化到 16 分音符网格96 ticks/quarter start_tick round(note.start * pm.resolution / 480 * 96) # 转换为 16 分音符单位 end_tick round(note.end * pm.resolution / 480 * 96) duration max(1, end_tick - start_tick) # 至少 1 个 16 分音符 notes.append({ pitch: note.pitch, start: start_tick, end: end_tick, duration: duration, velocity: min(127, max(30, note.velocity)) # 压缩力度范围避免极端值 }) return sorted(notes, keylambda x: x[start]) # 示例处理一首 Bach chorale clean_notes clean_midi_to_notes(bach_846.mid)逻辑说明pm.resolution是原始 MIDI 的 ticks per quarter通常 480我们将其映射到标准 16 分音符网格96 units per quarter使所有音符起止时间对齐统一节奏单元。velocity不是丢弃而是压缩到 [30,127] 区间——太弱30易被噪声淹没太强127在多数合成器中无意义且会扭曲模型对“强弱对比”的学习。2.2 构建多轨联合表征别再用单维 pitch 序列常见错误把所有音符按时间排序拼成[pitch1, pitch2, ...]一维序列。这完全破坏和声结构——C 和弦的 C-E-G 同时发声在一维序列里变成三个离散点模型无法感知“同时性”。正确方案是构建Piano Roll Voice Separation表征def build_pianoroll_with_voices(notes: list, max_time: int 4096, n_pitches128) - np.ndarray: # 初始化 (time_steps, n_pitches, n_voices4) —— 四声部合唱式建模 pianoroll np.zeros((max_time, n_pitches, 4), dtypenp.float32) # 按 start time 分组每组内按 pitch 排序分配到 voice 0~3最高音→voice0 time_groups {} for note in notes: t int(note[start]) if t max_time: continue if t not in time_groups: time_groups[t] [] time_groups[t].append(note) for t, group in time_groups.items(): # 每个时间点最多分配 4 个音四声部按 pitch 降序分 voice sorted_notes sorted(group, keylambda x: x[pitch], reverseTrue) for i, note in enumerate(sorted_notes[:4]): voice_idx i pitch_idx note[pitch] duration min(note[duration], max_time - t) # 在 pianoroll 上画出持续音符非仅 onset pianoroll[t:tduration, pitch_idx, voice_idx] 1.0 return pianoroll # 输出 shape: (4096, 128, 4) —— 时间×音高×声部 pianoroll build_pianoroll_with_voices(clean_notes)参数说明max_time4096对应 256 小节16 分音符单位下每小节 16 单位覆盖绝大多数古典/流行片段n_voices4是经验阈值——实测超过 4 声部时模型难以稳定分离且人类听觉对 4 层同时音色的辨识度骤降duration填充而非单点标记强制模型学习音符延续性避免生成“机关枪式”短音。2.3 引入结构标签把乐句、调性、终止式变成可监督信号纯音符序列缺乏音乐语法锚点。我们在数据中嵌入结构化元标签作为辅助监督信号标签类型编码方式用途小节边界bar_start[t] 1强制模型在 bar line 处重置状态避免跨小节节奏混乱调性中心key_label[t] one_hot(tonic, mode)如 C:maj0, C:min12用 Krumhansl-Schmuckler 算法从音符分布估算每小节更新一次终止式类型cadence[t] ∈ {authentic, plagal, half, deceptive}基于最后两小节和弦进行规则匹配标注为分类标签这些标签不参与生成但作为多任务 loss 的辅助分支显著提升生成段落的结构性。实测在composer_machinelearning项目中加入后生成乐句的终止感准确率从 58% 提升至 83%。3. 损失函数不能只靠交叉熵听觉感知损失的落地实现模型输出logits后传统做法是nn.CrossEntropyLoss直接算 token 预测误差。但这完全无视音乐的本质属性音高距离 ≠ 语义距离节奏偏差 ≠ 音符错误。例如预测 C4 → C#4 的误差远小于 C4 → G4五度跳进破坏旋律流畅性预测 16 分音符延迟 1 tick比预测错一个音高更难被察觉。我们必须引入感知加权。3.1 音高感知损失Melodic Distance Weighting定义音高距离权重矩阵W_pitch[i][j]其中i,j为 MIDI 音高0–127def build_pitch_weight_matrix() - np.ndarray: # 基于十二平均律计算半音阶距离但对协和音程纯四/五度、大三度降权 W np.ones((128, 128), dtypenp.float32) for i in range(128): for j in range(128): semitones abs(i - j) % 12 # 协和音程纯五度(7)、纯四度(5)、大三度(4)、小三度(3) 权重减半 if semitones in [0, 3, 4, 5, 7]: W[i][j] 0.5 # 不协和音程增四/减五(6)、大七(11) 权重翻倍 elif semitones in [6, 11]: W[i][j] 2.0 # 其余按半音数线性衰减 else: W[i][j] 1.0 0.1 * semitones return W W_pitch build_pitch_weight_matrix() # 在训练 loop 中使用 logits model(x) # shape: (B, T, 128) targets y # shape: (B, T) loss_pitch F.cross_entropy(logits.view(-1, 128), targets.view(-1), reductionnone) # 加权 weighted_loss (loss_pitch * W_pitch[targets.view(-1)].flatten()).mean()逻辑说明W_pitch不是固定常量而是根据 target pitch 动态索引权重——预测错一个音高时惩罚力度取决于“错得多离谱”。例如目标音是 C460模型预测 F#466半音差 6增四度权重为 2.0惩罚加倍若预测 G467差 7纯五度权重 0.5惩罚减半。这迫使模型优先保证协和进行而非盲目追求音高绝对准确。3.2 节奏感知损失Onset Deviation Penalty节奏误差需区分“提前”与“拖后”——人类对拖后容忍度更低拖拍懒散。我们定义 onset 偏差损失def onset_deviation_loss(pred_onsets: torch.Tensor, true_onsets: torch.Tensor, tolerance: float 0.1) - torch.Tensor: # pred_onsets, true_onsets: (B, T) 归一化到 [0,1] 的时间位置 diff pred_onsets - true_onsets # 拖后惩罚更重diff 0 时乘 1.5 weight torch.where(diff 0, torch.tensor(1.5), torch.tensor(1.0)) # 仅对偏差 tolerance 的点计算损失容忍微小抖动 mask torch.abs(diff) tolerance loss (weight * torch.abs(diff) * mask).sum() / mask.sum().clamp(min1e-6) return loss # 在模型输出中额外预测 onset offset回归头 onset_pred model_onset_head(hidden_states) # shape: (B, T) loss_rhythm onset_deviation_loss(onset_pred, true_onsets)参数说明tolerance0.1对应 10% 小节长度如 4/4 拍中容忍 0.4 拍避免模型过度拟合量化噪声weight对拖后diff 0施加 1.5 倍惩罚符合人类听觉心理实验结论Parncutt, 1994。3.3 多尺度频谱重建损失用 STFT 替代 raw audio 重建若最终输出需转 WAV直接重建 waveform 效果差。改用多分辨率 STFT loss类似 HiFi-GAN 思路from torchaudio.transforms import Spectrogram class MultiScaleSTFTLoss(nn.Module): def __init__(self, fft_sizes[1024, 2048, 4096], hop_sizes[256, 512, 1024]): super().__init__() self.stfts nn.ModuleList([ Spectrogram(n_fftn, hop_lengthh, powerNone) for n, h in zip(fft_sizes, hop_sizes) ]) def forward(self, pred_wav, true_wav): loss 0.0 for stft in self.stfts: pred_spec stft(pred_wav) true_spec stft(true_wav) # L1 on magnitude, L2 on complex loss torch.mean(torch.abs(pred_spec - true_spec)) loss torch.mean(torch.abs(pred_spec.real - true_spec.real)**2) return loss / len(self.stfts) stft_loss MultiScaleSTFTLoss() loss_audio stft_loss(model_output_wav, target_wav)为什么有效STFT 在不同尺度捕捉节奏轮廓低频 resolution、音色细节高频 resolution和瞬态冲击短 hop比单纯 waveform MSE 更贴合听觉感知。实测在create_music_py.py输出 wav 后接入此 loss生成音频的“乐器质感”提升显著尤其铜管/弦乐泛音更自然。4. 解码不是采样完事带约束的 Beam Search 与人工干预接口训练完模型model.generate()直接输出往往“合法但无聊”——重复乐句、缺乏发展、终止突兀。这是因为 greedy 或 top-k 采样只考虑局部最优忽略全局音乐逻辑。必须引入结构化解码约束。4.1 乐句级 Beam Search强制满足终止式与调性一致性修改标准 beam search加入两个硬约束终止式约束beam 中每个候选序列其末尾 2 小节必须匹配预设终止式音程模式如 authentic cadenceV-I → 五度下行 主音收束调性一致性约束整个 beam 的 pitch class histogram 必须满足当前调性分布如 C majorC/D/E/F/G/A/B 出现频率比 ≈ [1,0.5,0.75,0.5,0.75,0.5,0.75]def constrained_beam_search(model, input_ids, max_length512, num_beams5, key_profileNone, cadence_rulesNone): # 初始化 beams: [(score, tokens, state)] beams [(0.0, input_ids.clone(), model.get_init_state())] for step in range(max_length): candidates [] for score, tokens, state in beams: logits model(tokens[-1:], statestate) probs F.softmax(logits, dim-1) # 获取 top-k tokens topk_probs, topk_ids torch.topk(probs, k10, dim-1) for i, (p, tok_id) in enumerate(zip(topk_probs[0], topk_ids[0])): new_tokens torch.cat([tokens, tok_id.unsqueeze(0)]) new_score score torch.log(p) # 硬约束检查 if not is_valid_phrase_end(new_tokens, cadence_rules): continue # 违反终止式剪枝 if not matches_key_profile(new_tokens, key_profile): continue # 调性漂移剪枝 candidates.append((new_score, new_tokens, model.update_state(state, tok_id))) # 保留 top num_beams beams sorted(candidates, keylambda x: x[0], reverseTrue)[:num_beams] if any(is_complete_phrase(b[1]) for b in beams): break return max(beams, keylambda x: x[0])[1] # 在 generate 时调用 generated_ids constrained_beam_search(model, prompt_ids, key_profileC_MAJOR_PROFILE, cadence_rulesAUTHENTIC_CADENCE_RULES)逻辑说明is_valid_phrase_end()检查最后 32 个 token约 2 小节是否构成合法终止式matches_key_profile()计算当前序列的 pitch class 直方图与目标调性理论分布做 KL 散度阈值设为 0.15。这确保生成段落“有始有终”而非无限循环。4.2 实时人工干预接口用 MIDI 控制器修正生成流生成过程不应是黑匣子。我们在推理 pipeline 中嵌入MIDI Control Channel 监听允许用户用旋钮实时干预import mido class InteractiveGenerator: def __init__(self, model): self.model model self.port mido.open_input(APC40) # 读取 Ableton Push 类控制器 self.control_map { 1: tempo, # CC1 控制速度 2: tension, # CC2 控制和声紧张度增加属七/减七比例 3: density, # CC3 控制音符密度减少休止符比例 } def generate_with_control(self, prompt, duration_sec30): start_time time.time() while time.time() - start_time duration_sec: # 每 100ms 查询一次控制器 for msg in self.port.iter_pending(): if msg.type control_change and msg.control in self.control_map: self.set_control_param(self.control_map[msg.control], msg.value) # 模型生成下一小节注入 control 参数 next_bar self.model.step(prompt, tempoself.tempo, tensionself.tension, densityself.density) prompt torch.cat([prompt, next_bar]) yield next_bar # 使用示例连接硬件后实时调整 gen InteractiveGenerator(model) for bar in gen.generate_with_control(prompt_ids): play_midi_bar(bar) # 实时播放为什么必要专业作曲中“灵感修正”比“全自动”更重要。这个接口让composer_machinelearning项目从玩具升级为创作工具——用户转动旋钮模型即时响应CC2 拉高立刻插入一个属七和弦CC3 降低生成更多休止与留白。这才是“人机协同”的真实形态。5. 避坑指南自动音乐生成的 4 个血泪现场与根因修复以下是我在线上调试main.ipynb和create_music_py.py时反复踩中的坑。每一条都对应一个具体现象、根本原因和可立即执行的修复命令。5.1 现象生成的旋律突然“卡顿”连续多个小节音符全停原因MIDI 文件中存在note_off事件缺失导致pretty_midi解析时将长音符误判为无限延长后续音符因时间冲突被丢弃。模型学到“静音是安全选择”于是主动生成休止。解决预处理时强制补全note_off并设置最大音符时长# 在 clean_midi_to_notes() 中添加 for note in instrument.notes: if note.end - note.start 4.0: # 超过 4 小节强制截断 note.end note.start 4.0 # 若无 note_off用 start 0.5s 补全 if not hasattr(note, end) or note.end 0: note.end note.start 0.55.2 现象模型在训练后期 loss 突然飙升验证集 accuracy 断崖下跌原因create_music_py.py默认使用AdamW但学习率调度器ReduceLROnPlateau在 AMG 任务中极易早衰——因为 validation loss 波动大受解码随机性影响触发误降学习率模型陷入局部极小。解决改用CosineAnnealingLR并增大 warmup 步数scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max10000, eta_min1e-6 ) # warmup 前 1000 步线性增到峰值 for i in range(1000): lr base_lr * i / 1000 for param_group in optimizer.param_groups: param_group[lr] lr5.3 现象生成的和弦进行“数学正确但耳朵难受”如 C → F# → B原因模型只学音符共现频率未建模声部进行规则如避免平行五度、保持声部平稳。训练数据中若包含大量爵士即兴模型会习得跳跃进行但古典/流行语境下违和。解决在损失函数中加入Voice Leading Regularizationdef voice_leading_loss(hidden_states, prev_chord, curr_chord): # 计算当前 chord 与前一 chord 各声部移动距离 move_dist torch.norm(curr_chord - prev_chord, dim-1) # (B, 4) # 惩罚 2 半音的移动除 bass 外 penalty torch.where(move_dist[1:] 2, move_dist[1:], 0).sum() return penalty * 0.01 # 权重系数5.4 现象导出的 WAV 文件有高频嘶嘶声频谱显示 15kHz 以上能量异常原因create_music_py.py中默认用scipy.io.wavfile.write但未指定dtypenp.int16导致 float32 数据被截断产生量化噪声。解决导出前显式归一化并转 int16# 替换原 write 代码 audio_int16 (audio * 32767).astype(np.int16) # float [-1,1] → int16 scipy.io.wavfile.write(output.wav, sample_rate, audio_int16)6. 验证不是听一遍用 MusicXML 乐理规则引擎做自动化质检生成结果好不好不能只靠“我觉得还行”。我给自己定死一条规矩任何新模型上线前必须通过 MusicXML 自动化质检流水线。这套流程跑通才算真正落地。6.1 导出 MusicXML 并加载规则引擎pretty_midi可导出 MusicXML再用music21加载并运行乐理检查import music21 as m21 def export_and_validate(midi_path: str, rules: list): # 导出 MusicXML pm pretty_midi.PrettyMIDI(midi_path) mf m21.midi.translate.midiFilePathToStream(midi_path) mf.write(musicxml, fptemp.xml) # 加载并运行规则 s m21.converter.parse(temp.xml) results {} for rule_name, rule_func in rules: try: results[rule_name] rule_func(s) except Exception as e: results[rule_name] fERROR: {str(e)} return results # 定义质检规则 def check_parallel_fifths(s): 检测平行五度/八度 parallel_count 0 for part1, part2 in zip(s.parts, s.parts[1:]): for n1, n2 in zip(part1.flat.notes, part2.flat.notes): if n1.offset n2.offset and n1.duration.quarterLength 0.25: interval m21.interval.Interval(n1, n2) if interval.name in [P5, P8]: parallel_count 1 return parallel_count 0 def check_phrase_balance(s): 检测乐句长度是否为 2/4/8 小节符合古典惯例 total_measures len(list(s.recurse().getElementsByClass(Measure))) return total_measures in [2, 4, 8, 16] rules [ (No Parallel Fifths, check_parallel_fifths), (Balanced Phrases, check_phrase_balance), (Cadence Validity, lambda s: len(list(s.recurse().getElementsByClass(Cadence))) 0), ]6.2 构建可配置的质检报告表每次生成后自动生成 HTML 报告包含可视化与分数规则项状态详情权重扣分No Parallel Fifths✅ PASS未检测到平行五度30%0Balanced Phrases⚠️ WARN检测到 6 小节乐句非常规25%-5Cadence Validity✅ PASS检测到 authentic cadence25%0Voice Independence❌ FAILTenor 与 Bass 声部重叠率 42%20%-20综合得分100%75/100关键技巧music21的Cadence类需手动标注终止式位置我用正则匹配和弦符号如V7/I自动打标Voice Independence用s.parts[i].flat.notes计算各声部音符重叠率阈值设为 30%。这份报告不是摆设——当综合得分 70自动拒绝该生成结果触发 re-sample。6.3 我的日常质检习惯三遍验证法第一遍机器跑 MusicXML 规则引擎过滤掉硬伤平行五度、无终止式第二遍耳朵用 Audacity 加载 WAV关闭视觉纯听 3 遍第一遍抓节奏感第二遍抓和声走向第三遍抓动态起伏第三遍乐谱打开 MuseScore 查看生成的 MusicXML重点看声部交叉如 soprano 低于 alto和谱面可读性连音线是否合理、休止符位置是否符合视唱习惯。这三遍下来基本能筛掉 90% 的“技术正确但音乐失败”样本。曾经有个模型 loss 低到 0.01但第三遍看谱发现 bass 声部全程在 high register完全违背功能和声原则——机器没报错眼睛一眼揪出。希望帮到你。本文还有配套的精品资源点击获取
