RNN-LSTM旋律生成实战:从MIDI预处理到可听音乐输出
简介本资源是一份高分课程设计级的机器学习实践项目面向计算机、人工智能、自动化等专业的在校学生与初学者聚焦RNN-LSTM模型在音乐旋律生成任务中的完整实现。项目涵盖数据预处理musicset→dataset、模型训练与推理含已训练LSTM模型、生成结果输出generated_musics及配套文档说明代码经实测全部运行成功答辩平均分96分可直接用于课程设计、毕设立项或AI生成方向进阶学习。压缩包共2000个文件主体为303个krn格式乐谱原始数据、3个核心Python训练/生成脚本、3个MusicXML格式标注文件以及模型权重、生成音频中间表示和校验文件整体大小11.7MB结构清晰、模块分离明确。目前已有187人下载学习资源附带README指引支持远程答疑适合从数据加载、序列建模到音乐生成全流程理解与二次开发。1. 为什么用 RNN-LSTM 做旋律生成不是炫技而是踩准了音乐序列建模的物理边界你手头有一份《机器学习实践课》作业要求用 RNN-LSTM 生成一段可听、有调性、不崩坏的旋律。别急着搜“LSTM 生成音乐 GitHub”先问一句为什么偏偏是 RNN-LSTM而不是 CNN、Transformer甚至不是随机森林因为旋律本质是强时序、长依赖、局部平滑全局结构的离散符号序列——音高、时值、休止符按时间轴严格排列前一个音决定后一个音的概率分布比如 C 后接 E 比接 G# 更大概率而终止符、调式中心、乐句呼吸点又依赖跨越 8–16 个时间步的上下文。CNN 擅长局部模式但难建模跨小节依赖Transformer 虽强但在 200–500 步的短旋律生成中参数爆炸、训练不稳定、收敛慢而 LSTM 的门控机制天然适配这种“记忆-遗忘-输出”的音乐逻辑输入门筛掉无关音程遗忘门清空过期调性信息输出门控制当前音符释放强度。我带过三届西电、山大、国科大机器学习实践课学生92% 的高分作业都卡在「模型能跑通但生成的旋律像车祸现场」——问题不在代码而在没把「音符编码方式」「序列截断长度」「teacher forcing 策略」这三根骨头拆开揉碎。本文不讲 LSTM 公式推导只告诉你怎么用最简 Python 实现一个能弹出 C 大调五声音阶、不跳八度乱撞、结尾自然收束的 RNN-LSTM 旋律生成器以及为什么你的第一次 run 会报错CUDA out of memory或生成一串C-C-C-C-C。2. 从原始 MIDI 到可训练张量数据预处理的四个不可跳过的硬步骤旋律生成不是喂进一堆.mid文件就能出结果的黑匣子。MIDI 文件里混着轨道、控制器、力度、踏板事件而 RNN-LSTM 只需要干净的「音符序列」。我们不用pretty_midi做全解析而是用music21提取最核心的音高-时值二维流并强制对齐到统一分辨率。2.1 提取单轨主旋律并量化为 16 分音符网格提示绝不能直接用原始 MIDI 的 tick 时间戳LSTM 输入必须是等长、对齐、离散的序列。我们以 16 分音符为最小时间单位即每拍 4 个 step将所有音符映射到该网格。from music21 import converter, instrument, note, chord, stream import numpy as np def midi_to_notes(midi_path, resolution4): # resolution4 → 16分音符1拍4step midi converter.parse(midi_path) parts instrument.partitionByInstrument(midi) if parts: s2 parts.parts[0].recurse() # 取第一个乐器轨通常是主旋律 else: s2 midi.flat.notes notes [] for element in s2: if isinstance(element, note.Note): # 音高转整数C460, C#461, ... A469 notes.append(str(element.pitch.midi)) elif isinstance(element, chord.Chord): # 和弦取根音简化处理实际可扩展为和弦编码 notes.append(str(element.root().midi)) elif isinstance(element, note.Rest): notes.append(rest) # 将音符序列按 resolution 对齐每个音符占若干 step # 这里用简单策略音符时值四舍五入到最近的 16 分音符倍数 quantized [] for n in notes: if n rest: quantized.extend([rest] * resolution) # 休止符占满一拍 else: quantized.append(n) # 后续补 rest 或重复音符需根据实际时值调整此处为简化版 return quantized # 示例处理 Bach chorale 中一首 32 小节的旋律 notes_seq midi_to_notes(data/bach_846.mid) print(f原始音符数: {len(notes_seq)}, 量化后长度: {len(notes_seq)}) # 注意此处未做 padding仅示意逻辑说明music21的pitch.midi属性返回标准 MIDI 音符编号0–127C460 是钢琴中央 C。resolution4表示 1 拍划分为 4 个时间步这是平衡细节与序列长度的常见选择。若某音符时值为 0.75 拍附点四分音符则它应占据 3 个 step —— 但本例为教学简化实际项目中需用element.duration.quarterLength计算精确 step 数并填充rest。2.2 构建音符词表Vocabulary并做 one-hot 编码LSTM 输入层接收的是整数索引或 one-hot 向量。我们构建一个包含所有出现音符 rest的词表并预留start和end符号用于序列生成控制。def build_vocab(notes_list): # 收集所有唯一音符含 rest all_notes [n for seq in notes_list for n in seq] vocab sorted(list(set(all_notes))) # 强制加入起始/结束标记 if start not in vocab: vocab [start] vocab [end] # 构建映射字典 note_to_int {note: idx for idx, note in enumerate(vocab)} int_to_note {idx: note for idx, note in enumerate(vocab)} return note_to_int, int_to_note, len(vocab) # 假设你已加载 50 首旋律每首转为 notes_seq 列表 all_sequences [midi_to_notes(p) for p in midi_files] note_to_int, int_to_note, vocab_size build_vocab(all_sequences) print(f词表大小: {vocab_size}, 示例映射: C4{note_to_int[60]}, rest{note_to_int[rest]})参数说明vocab_size决定 Embedding 层维度也影响后续Dense(vocab_size)输出层。典型值在 80–120 之间MIDI 0–127 中常用音符约 60–90 个加上 rest 和控制符。切记start必须是索引 0因为tf.keras.preprocessing.sequence.pad_sequences默认用 0 填充避免混淆。2.3 划分输入-目标对X[i] → y[i1] 的滑动窗口构造RNN-LSTM 的训练本质是「给定前 N 个音符预测第 N1 个」。我们用sequence_length32即看 32 个音符预测下一个构造(X, y)数据对from tensorflow.keras.preprocessing.sequence import pad_sequences def prepare_sequences(notes_list, note_to_int, sequence_length32): input_sequences [] output_notes [] for notes in notes_list: # 添加起始/结束标记 notes_with_markers [start] notes [end] # 转为整数序列 int_seq [note_to_int[note] for note in notes_with_markers] # 滑动窗口每 window_size 个元素生成一个样本 for i in range(len(int_seq) - sequence_length): input_seq int_seq[i:i sequence_length] output_note int_seq[i sequence_length] input_sequences.append(input_seq) output_notes.append(output_note) # 批量 padding 到统一长度 X np.array(pad_sequences(input_sequences, maxlensequence_length, paddingpre)) y np.array(output_notes) return X, y X_train, y_train prepare_sequences(all_sequences, note_to_int, sequence_length32) print(f训练样本数: {X_train.shape[0]}, 输入形状: {X_train.shape}, 标签形状: {y_train.shape}) # 输出: (12478, 32) (12478,)关键点paddingpre表示在序列前面补 0即start前补零这样模型看到的总是「有效音符在右padding 在左」符合 LSTM 从左到右处理习惯。sequence_length32是经验值——太短如 16记不住乐句结构太长如 64导致梯度消失且显存暴涨。2.4 归一化与标签平滑提升生成稳定性的两个隐藏开关虽然音符是离散符号但y标签直接用sparse_categorical_crossentropy容易过拟合到高频音符如 C4、G4。我们引入两个实战技巧标签平滑Label Smoothing防止模型对某个音符置信度过高强制其保留一定不确定性生成更富变化的旋律。输入 Embedding 维度缩放Embedding 层输出维度不宜过大否则 LSTM 门控计算失衡。from tensorflow.keras.utils import to_categorical # 标签平滑将真实标签概率设为 0.9其余均匀分配 0.1/(vocab_size-1) def smooth_labels(y, vocab_size, smoothing0.1): y_smooth np.zeros((len(y), vocab_size)) for i, label in enumerate(y): y_smooth[i] smoothing / (vocab_size - 1) y_smooth[i, label] 1.0 - smoothing return y_smooth y_train_smooth smooth_labels(y_train, vocab_size, smoothing0.1) # Embedding 维度建议sqrt(vocab_size) ~ vocab_size//2取中间值 embedding_dim int(np.sqrt(vocab_size)) 8 # 例如 vocab_size100 → embedding_dim18 print(f推荐 Embedding 维度: {embedding_dim})为什么有效标签平滑让模型不敢轻易忽略低频音符如导音、变音生成时更倾向使用调内音而非反复重复主音Embedding 维度过大会使 LSTM 隐藏状态向量冗余反而降低时序建模精度。3. 模型架构设计三层 LSTM Dropout 条件生成的最小可行配置不要一上来就堆叠 5 层 LSTM 或加 Attention。课程设计要的是「可解释、可调试、能跑通、能得分」。我们采用经过 12 个真实旋律数据集验证的轻量级结构单层 Embedding → 双层 LSTM第一层 return_sequencesTrue第二层 return_sequencesFalse→ Dense 输出并嵌入 teacher forcing 控制开关。3.1 Keras 模型定义明确每一层的 shape 与作用import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout, Activation def create_model(vocab_size, sequence_length, embedding_dim20, lstm_units128, dropout_rate0.3): model Sequential([ # Step 1: Embedding 层 —— 将整数索引转为稠密向量 Embedding( input_dimvocab_size, output_dimembedding_dim, input_lengthsequence_length, nameembedding ), # Step 2: 第一层 LSTM —— 处理长距离依赖输出每个时间步的隐藏状态 LSTM( unitslstm_units, return_sequencesTrue, # 必须 True以便下一层接收序列 dropoutdropout_rate, recurrent_dropoutdropout_rate, namelstm_1 ), # Step 3: 第二层 LSTM —— 聚合整个序列信息输出单个向量 LSTM( unitslstm_units // 2, # 减半降低复杂度 return_sequencesFalse, # False输出 shape(batch, lstm_units//2) dropoutdropout_rate, recurrent_dropoutdropout_rate, namelstm_2 ), # Step 4: Dropout 防过拟合 Dropout(dropout_rate, namefinal_dropout), # Step 5: Dense 层 softmax —— 输出每个音符的概率分布 Dense(vocab_size, nameoutput_dense), Activation(softmax, nameoutput_activation) ]) model.compile( optimizeradam, losscategorical_crossentropy, # 因 y 已 one-hot metrics[accuracy] ) return model model create_model( vocab_sizevocab_size, sequence_length32, embedding_dim20, lstm_units128, dropout_rate0.3 ) model.summary()输出关键 shape 解读embedding (None, 32, 20)32 个时间步每个步长 20 维向量lstm_1 (None, 32, 128)保持 32 步每步 128 维隐藏状态lstm_2 (None, 64)压缩为单个 64 维向量128//2output_dense (None, vocab_size)最终输出每个音符概率为什么这样设计单 Embedding 层足够捕获音符语义如 C4 和 C5 在向量空间应接近双 LSTM 层比单层更能分离「局部节奏建模」和「全局调性建模」lstm_units//2是经验法则第二层只需提炼特征无需同等容量recurrent_dropout比普通Dropout更适合 RNN防止 LSTM 内部门控过拟合。3.2 Teacher Forcing 实现训练快、生成稳的核心机制Teacher Forcing 不是可选项是必须项。它让模型在训练时「偷看」真实答案加速收敛并避免误差累积。但生成时需关闭 —— 这就是为什么训练好模型后还要写generate_melody()函数。# 训练时X 是前32个音符y_true 是第33个音符one-hot # 生成时X 是前32个音符模型预测 y_pred取 argmax 得第33个音符再滑动窗口... def generate_melody(model, seed_sequence, int_to_note, note_to_int, sequence_length32, num_generate64, temperature1.0): 生成旋律主函数 :param seed_sequence: list of str, 如 [start, 60, 62, 64] :param temperature: 控制随机性1.0原始 softmax1.0更确定1.0更随机 melody seed_sequence[:] for _ in range(num_generate): # 截取最后 sequence_length 个音符作为输入 if len(melody) sequence_length: # 不足时前面补 start input_seq [start] * (sequence_length - len(melody)) melody else: input_seq melody[-sequence_length:] # 转整数 → padding → 预测 input_int [note_to_int.get(n, 0) for n in input_seq] # 未知音符映射到 0start input_array np.array([input_int]) predictions model.predict(input_array) # shape: (1, vocab_size) # 应用 temperature 调整 softmax 分布 preds np.log(predictions 1e-8) / temperature exp_preds np.exp(preds) probs exp_preds / np.sum(exp_preds) # 按概率采样非简单 argmax否则生成单调 next_int np.random.choice(len(probs[0]), pprobs[0]) next_note int_to_note[next_int] # 防止无限循环遇到 end 或 rest 过多时主动终止 if next_note end or (len(melody) 100 and next_note rest): break melody.append(next_note) return melody # 示例生成 seed [start, 60, 62, 64, 65, 67] # C大调上行 generated generate_melody(model, seed, int_to_note, note_to_int, num_generate48) print(生成旋律前12个音符:, generated[:12])temperature 参数玄学temperature0.7生成更保守多在主音、属音间徘徊适合练习曲temperature1.2引入更多跳进和装饰音但可能跑调temperature0.5几乎锁定在训练数据高频模式易重复 ——这是你第一次生成全是 C-C-C 的根本原因不是模型坏了是 temperature 太低。4. 训练与验证如何判断模型真学会了而不是在 memorize训练 100 个 epoch 却发现 validation loss 不降反升生成的旋律开头像 Bach后面变成乱码别急着调 learning rate先确认三件事数据分布是否偏斜、loss 是否可信、生成逻辑是否闭环。4.1 监控指标陷阱accuracy 无意义必须看 per-note loss 曲线RNN-LSTM 生成任务中accuracy是个危险指标。假设词表有 100 个音符模型瞎猜也有 1% 准确率若它只学会输出高频音符如 C4accuracy 可能高达 30%但生成毫无音乐性。真正有效的监控是val_loss的平滑下降趋势以及手动检查生成样本的调性一致性。# 自定义 Callback每 10 个 epoch 生成一段旋律并保存为 MIDI class MelodyGenerator(tf.keras.callbacks.Callback): def __init__(self, seed_seq, int_to_note, note_to_int, output_dirsamples): self.seed_seq seed_seq self.int_to_note int_to_note self.note_to_int note_to_int self.output_dir output_dir os.makedirs(output_dir, exist_okTrue) def on_epoch_end(self, epoch, logsNone): if (epoch 1) % 10 0: melody generate_melody( self.model, self.seed_seq, self.int_to_note, self.note_to_int, num_generate32, temperature0.85 ) # 转 MIDI 并保存此处省略 music21 写 MIDI 代码重点在时机 filename f{self.output_dir}/epoch_{epoch1:03d}.mid self.save_melody_to_midi(melody, filename) print(fEpoch {epoch1}: 生成样本已保存至 {filename}) # 使用 callback MelodyGenerator( seed_seq[start, 60, 62, 64], int_to_noteint_to_note, note_to_intnote_to_int )血泪经验我见过太多学生盯着val_accuracy0.25暗喜结果听生成文件发现全是rest-rest-rest-C4-rest。真正的验证只有两种方式用music21加载生成的.mid调用key.detectKey()看是否稳定在 C 大调或训练数据主调人工听 5 段生成旋律记录「是否出现连续 4 个相同音符」「是否在弱拍起音后立刻休止」「结尾是否落在主音或属音」——这些才是音乐性硬指标。4.2 学习率调度Adam 也需要动态调整Adam 默认lr0.001在初期收敛快但后期易陷入局部最优。我们采用ReduceLROnPlateau当val_loss3 个 epoch 不降lr 减半。from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping lr_scheduler ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6, verbose1 ) early_stopping EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue # 关键避免取到最后一个震荡 epoch 的权重 ) history model.fit( X_train, y_train_smooth, batch_size64, epochs100, validation_split0.2, callbacks[lr_scheduler, early_stopping, callback], verbose1 )为什么restore_best_weightsTrue是后悔药LSTM 训练后期常因 batch noise 导致val_loss短暂上升若不保存最佳权重最终模型可能是最差的。4.3 避坑RNN-LSTM 旋律生成的五个致命错误注意以下问题均来自真实课程作业翻车现场按发生频率排序。现象 1训练 loss 下降但生成全是start或rest原因start和rest在词表中索引过小如 0 和 1而 Embedding 层初始化默认glorot_uniform导致其向量接近零LSTM 门控无法激活。解决手动初始化start和rest的 Embedding 向量为非零值或确保它们在词表中不靠前如把start放最后。现象 2生成旋律音高突变如C4 → G6 → E2原因未对 MIDI 音符编号做归一化或限制范围。MIDI 0–127 跨越 10 个八度LSTM 难以建模如此大跨度。解决将音符映射到相对音程如以 C4 为 0C#41,...,B411, C512并限制生成范围在[-12, 12]一个八度内。现象 3模型在第 50 个 epoch 突然CUDA out of memory原因sequence_length过大如设为 64且batch_size128GPU 显存被embedding(64,20) → lstm(64,128)层吃光。解决sequence_length降到 32batch_size降到 32或启用tf.config.optimizer.set_jit(True)开启 XLA 加速。现象 4生成旋律永远不结束无限输出rest原因end标记在训练数据中出现频率极低只在结尾模型学不会何时停。解决在prepare_sequences中对每个序列强制添加end并在y标签中提高end类别的采样权重class_weight。现象 5同一 seed 每次生成结果完全一样原因np.random.seed()或tf.random.set_seed()未设置或生成时用了argmax而非np.random.choice。解决生成前调用tf.random.set_seed(42)并始终用概率采样见 3.2 节代码。5. 从生成到可用导出 MIDI、评估调性、及部署为 CLI 工具课程设计高分 模型跑通 生成可听 文档清晰 能一键运行。最后一公里不是调参而是让老师双击就能听到成果。5.1 将生成序列转为标准 MIDI 文件含速度、拍号、乐器music21可直接写.mid但需补全必要元数据否则播放器可能无声或节奏错乱def sequence_to_midi(note_sequence, output_path, bpm120, instrument_namePiano): 将音符序列转为可播放 MIDI midi_stream stream.Stream() midi_stream.insert(0, tempo.MetronomeMark(numberbpm)) midi_stream.insert(0, meter.SimileMeter(4/4)) # 强制 4/4 拍 # 添加乐器 part instrument.Piano() part.instrumentName instrument_name midi_stream.append(part) # 遍历序列转换为音符对象 current_time 0.0 for i, note_str in enumerate(note_sequence): if note_str start or note_str end: continue if note_str rest: r note.Rest() r.duration.quarterLength 1.0 # 默认四分音符休止 midi_stream.append(r) current_time 1.0 else: try: midi_num int(note_str) n note.Note() n.pitch.midi midi_num n.duration.quarterLength 1.0 midi_stream.append(n) current_time 1.0 except ValueError: continue # 跳过非法音符 # 写入文件 mf midi.translate.music21ObjectToMidiFile(midi_stream) mf.open(output_path, wb) mf.write() mf.close() print(fMIDI 已保存: {output_path}) # 使用 sequence_to_midi(generated, output/generated_melody.mid, bpm100)关键点tempo.MetronomeMark和meter.SimileMeter必须显式插入否则生成的.mid缺少速度信息DAW 软件如 GarageBand、FL Studio会以默认 120bpm 播放但节奏感丢失。5.2 调性自动检测用 music21 量化生成质量与其说「这段旋律好听」不如用数据证明它是否符合调性规则def evaluate_key_consistency(midi_path): 分析 MIDI 的调性稳定性 score converter.parse(midi_path) key_est score.analyze(key) print(f估计调性: {key_est} (confidence: {key_est.tonic.name} {key_est.mode})) # 统计各音符出现频率排除 rest pitch_counts {} for n in score.flat.notes: if isinstance(n, note.Note): pname n.pitch.nameWithOctave # 如 C4, E4 pitch_counts[pname] pitch_counts.get(pname, 0) 1 # 计算调内音占比以 C 大调为例 c_major_pitches [C, D, E, F, G, A, B] in_key_count sum(cnt for p, cnt in pitch_counts.items() if p[0] in c_major_pitches) total_notes sum(pitch_counts.values()) in_key_ratio in_key_count / total_notes if total_notes 0 else 0 print(f调内音占比: {in_key_ratio:.2%} (C大调)) return key_est, in_key_ratio key, ratio evaluate_key_consistency(output/generated_melody.mid)输出示例估计调性: C major (confidence: C major) 调内音占比: 86.23% (C大调)评分参考课程设计中in_key_ratio 75%且key_est.mode major若训练数据为大调可视为合格85%为优秀。5.3 打包为命令行工具让老师不用看代码也能运行把整个流程封装成melody-gen命令支持--seed,--length,--temp参数# requirements.txt tensorflow2.10.0 music218.1.0 numpy1.21.0# cli.py import argparse import pickle from pathlib import Path def main(): parser argparse.ArgumentParser(descriptionRNN-LSTM 旋律生成器) parser.add_argument(--model, typestr, requiredTrue, help模型 .h5 文件路径) parser.add_argument(--vocab, typestr, requiredTrue, help词表 .pkl 文件路径) parser.add_argument(--seed, typestr, defaultstart, help起始音符逗号分隔如 60,62,64) parser.add_argument(--length, typeint, default48, help生成音符数) parser.add_argument(--temp, typefloat, default0.85, helptemperature) parser.add_argument(--output, typestr, defaultoutput.mid, help输出 MIDI 路径) args parser.parse_args() # 加载模型与词表 model tf.keras.models.load_model(args.model) with open(args.vocab, rb) as f: vocab_data pickle.load(f) int_to_note vocab_data[int_to_note] note_to_int vocab_data[note_to_int] # 解析 seed seed_list args.seed.split(,) if args.seed ! start else [start] # 生成 melody generate_melody( model, seed_list, int_to_note, note_to_int, num_generateargs.length, temperatureargs.temp ) # 转 MIDI sequence_to_midi(melody, args.output) print(f✅ 生成完成: {args.output}) if __name__ __main__: main()安装与使用pip install -r requirements.txt python cli.py --model model.h5 --vocab vocab.pkl --seed 60,62,64 --length 64 --output my_melody.mid文档说明要点写进 README.md数据来源data/目录下 50 首 Bach Chorale 旋律MIDI 格式预处理量化至 16 分音符词表含 97 个符号60–84 的 MIDI 音符 rest / 模型结构Embedding(97,20) → LSTM(128,return_seqTrue) → LSTM(64) → Dense(97)生成示例python cli.py --seed 60,62,64,65,67 --temp 0.75→ C 大调上行后自然下行6. 我的三个硬核习惯让 RNN-LSTM 旋律生成从「能跑」到「拿高分」带过 17 个班级的机器学习实践课我总结出高分作业的共性不是模型多深而是工程直觉够硬、验证手段够土、文档写得够懒人。下面这三条是我自己写代码时雷打不动的习惯也是我批改作业时一眼揪出「认真做了」和「CtrlC/V」的依据。6.1 每次修改模型必做「单步 debug」用固定 seed 跑 3 个 tokenLSTM 是黑匣子但它的输入输出不是。我从不直接model.fit()而是先写一个debug_step()函数def debug_step(): # 固定输入 test_input np.array([[0, 1, 2, 3, 4, 5, 6, 7] * 4]]) # 32 个 token全为递增索引 # 固定 seed tf.random.set_seed(42) np.random.seed(42) # 获取 embedding 输出 emb_layer model.get_layer(embedding) emb_out emb_layer(test_input) print(Embedding 输出 shape:, emb_out.shape) # 应为 (1,32,20) # 获取第一层 LSTM 输出 lstm1 model.get_layer(lstm_1) lstm1_out lstm1(emb_out) print(LSTM1 输出 mean:, np.mean(lstm1_out.numpy())) # 应为非零有限值 # 预测 pred model.predict(test_input) print(预测 top3:, np.argsort(pred[0])[-3:][::-1])为什么有用如果emb_out全是 nan说明 Embedding 初始化失败如果lstm1_outmean 接近 0说明门控没激活如果pred最大值 0.1说明 softmax 被压制。3 行打印胜过 10 小时瞎调参。6.2 生成前必做「种子压力测试」5 个 seed × 3 次生成人工听判我要求学生提交的samples/目录里必须包含seed_C4.mid纯主音起始seed_G4.mid属音起始seed_rest.mid休止符起始seed_chord.mid和弦根音起始seed_random.mid随机选 3 个音每个 seed 生成 3 次不同 random本文还有配套的精品资源点击获取