news 2026/9/23 15:05:08

RNN-LSTM旋律生成实战:从MIDI预处理到可听音乐输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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, resolution=4): # resolution=4 → 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): # 音高转整数:C4=60, C#4=61, ... A4=69 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,仅示意

逻辑说明music21pitch.midi属性返回标准 MIDI 音符编号(0–127),C4=60 是钢琴中央 C。resolution=4表示 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[i+1] 的滑动窗口构造

RNN-LSTM 的训练本质是「给定前 N 个音符,预测第 N+1 个」。我们用sequence_length=32(即看 32 个音符预测下一个),构造(X, y)数据对:

from tensorflow.keras.preprocessing.sequence import pad_sequences def prepare_sequences(notes_list, note_to_int, sequence_length=32): 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, maxlen=sequence_length, padding='pre')) y = np.array(output_notes) return X, y X_train, y_train = prepare_sequences(all_sequences, note_to_int, sequence_length=32) print(f"训练样本数: {X_train.shape[0]}, 输入形状: {X_train.shape}, 标签形状: {y_train.shape}") # 输出: (12478, 32) (12478,)

关键点padding='pre'表示在序列前面补 0(即<start>前补零),这样模型看到的总是「有效音符在右,padding 在左」,符合 LSTM 从左到右处理习惯。sequence_length=32是经验值——太短(如 16)记不住乐句结构,太长(如 64)导致梯度消失且显存暴涨。

2.4 归一化与标签平滑:提升生成稳定性的两个隐藏开关

虽然音符是离散符号,但y标签直接用sparse_categorical_crossentropy容易过拟合到高频音符(如 C4、G4)。我们引入两个实战技巧:

  1. 标签平滑(Label Smoothing):防止模型对某个音符置信度过高,强制其保留一定不确定性,生成更富变化的旋律。
  2. 输入 Embedding 维度缩放:Embedding 层输出维度不宜过大,否则 LSTM 门控计算失衡。
from tensorflow.keras.utils import to_categorical # 标签平滑:将真实标签概率设为 0.9,其余均匀分配 0.1/(vocab_size-1) def smooth_labels(y, vocab_size, smoothing=0.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, smoothing=0.1) # Embedding 维度建议:sqrt(vocab_size) ~ vocab_size//2,取中间值 embedding_dim = int(np.sqrt(vocab_size)) + 8 # 例如 vocab_size=100 → embedding_dim=18 print(f"推荐 Embedding 维度: {embedding_dim}")

为什么有效:标签平滑让模型不敢轻易忽略低频音符(如导音、变音),生成时更倾向使用调内音而非反复重复主音;Embedding 维度过大会使 LSTM 隐藏状态向量冗余,反而降低时序建模精度。


3. 模型架构设计:三层 LSTM + Dropout + 条件生成的最小可行配置

不要一上来就堆叠 5 层 LSTM 或加 Attention。课程设计要的是「可解释、可调试、能跑通、能得分」。我们采用经过 12 个真实旋律数据集验证的轻量级结构:单层 Embedding → 双层 LSTM(第一层 return_sequences=True,第二层 return_sequences=False)→ 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_dim=20, lstm_units=128, dropout_rate=0.3): model = Sequential([ # Step 1: Embedding 层 —— 将整数索引转为稠密向量 Embedding( input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length, name='embedding' ), # Step 2: 第一层 LSTM —— 处理长距离依赖,输出每个时间步的隐藏状态 LSTM( units=lstm_units, return_sequences=True, # 必须 True,以便下一层接收序列 dropout=dropout_rate, recurrent_dropout=dropout_rate, name='lstm_1' ), # Step 3: 第二层 LSTM —— 聚合整个序列信息,输出单个向量 LSTM( units=lstm_units // 2, # 减半,降低复杂度 return_sequences=False, # False,输出 shape=(batch, lstm_units//2) dropout=dropout_rate, recurrent_dropout=dropout_rate, name='lstm_2' ), # Step 4: Dropout 防过拟合 Dropout(dropout_rate, name='final_dropout'), # Step 5: Dense 层 + softmax —— 输出每个音符的概率分布 Dense(vocab_size, name='output_dense'), Activation('softmax', name='output_activation') ]) model.compile( optimizer='adam', loss='categorical_crossentropy', # 因 y 已 one-hot metrics=['accuracy'] ) return model model = create_model( vocab_size=vocab_size, sequence_length=32, embedding_dim=20, lstm_units=128, dropout_rate=0.3 ) model.summary()

输出关键 shape 解读

  • embedding (None, 32, 20):32 个时间步,每个步长 20 维向量
  • lstm_1 (None, 32, 128):保持 32 步,每步 128 维隐藏状态
  • lstm_2 (None, 64):压缩为单个 64 维向量(128//2)
  • output_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_length=32, num_generate=64, temperature=1.0): """ 生成旋律主函数 :param seed_sequence: list of str, 如 ['<start>', '60', '62', '64'] :param temperature: 控制随机性,1.0=原始 softmax,<1.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] # 未知音符映射到 0(<start>) 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]), p=probs[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_generate=48) print("生成旋律(前12个音符):", generated[:12])

temperature 参数玄学

  • temperature=0.7:生成更保守,多在主音、属音间徘徊,适合练习曲;
  • temperature=1.2:引入更多跳进和装饰音,但可能跑调;
  • temperature=0.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% 准确率;若它只学会输出高频音符(如 C4),accuracy 可能高达 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_dir="samples"): 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_ok=True) def on_epoch_end(self, epoch, logs=None): if (epoch + 1) % 10 == 0: melody = generate_melody( self.model, self.seed_seq, self.int_to_note, self.note_to_int, num_generate=32, temperature=0.85 ) # 转 MIDI 并保存(此处省略 music21 写 MIDI 代码,重点在时机) filename = f"{self.output_dir}/epoch_{epoch+1:03d}.mid" self.save_melody_to_midi(melody, filename) print(f"Epoch {epoch+1}: 生成样本已保存至 {filename}") # 使用 callback = MelodyGenerator( seed_seq=['<start>', '60', '62', '64'], int_to_note=int_to_note, note_to_int=note_to_int )

血泪经验:我见过太多学生盯着val_accuracy=0.25暗喜,结果听生成文件发现全是rest-rest-rest-C4-rest真正的验证只有两种方式

  1. music21加载生成的.mid,调用key.detectKey()看是否稳定在 C 大调(或训练数据主调);
  2. 人工听 5 段生成旋律,记录「是否出现连续 4 个相同音符」「是否在弱拍起音后立刻休止」「结尾是否落在主音或属音」——这些才是音乐性硬指标。

4.2 学习率调度:Adam 也需要动态调整

Adam 默认lr=0.001在初期收敛快,但后期易陷入局部最优。我们采用ReduceLROnPlateau:当val_loss3 个 epoch 不降,lr 减半。

from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping lr_scheduler = ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1 ) early_stopping = EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True # 关键!避免取到最后一个震荡 epoch 的权重 ) history = model.fit( X_train, y_train_smooth, batch_size=64, epochs=100, validation_split=0.2, callbacks=[lr_scheduler, early_stopping, callback], verbose=1 )

为什么restore_best_weights=True是后悔药: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 为 0,C#4=1,...,B4=11, C5=12),并限制生成范围在[-12, +12](一个八度内)。

现象 3:模型在第 50 个 epoch 突然CUDA out of memory

原因sequence_length过大(如设为 64)且batch_size=128,GPU 显存被embedding(64,20) → lstm(64,128)层吃光。
解决sequence_length降到 32,batch_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, bpm=120, instrument_name="Piano"): """将音符序列转为可播放 MIDI""" midi_stream = stream.Stream() midi_stream.insert(0, tempo.MetronomeMark(number=bpm)) 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(f"MIDI 已保存: {output_path}") # 使用 sequence_to_midi(generated, "output/generated_melody.mid", bpm=100)

关键点tempo.MetronomeMarkmeter.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 tensorflow>=2.10.0 music21>=8.1.0 numpy>=1.21.0
# cli.py import argparse import pickle from pathlib import Path def main(): parser = argparse.ArgumentParser(description="RNN-LSTM 旋律生成器") parser.add_argument("--model", type=str, required=True, help="模型 .h5 文件路径") parser.add_argument("--vocab", type=str, required=True, help="词表 .pkl 文件路径") parser.add_argument("--seed", type=str, default="<start>", help="起始音符,逗号分隔,如 '60,62,64'") parser.add_argument("--length", type=int, default=48, help="生成音符数") parser.add_argument("--temp", type=float, default=0.85, help="temperature") parser.add_argument("--output", type=str, default="output.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_generate=args.length, temperature=args.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_seq=True) → LSTM(64) → Dense(97)
  • 生成示例:python cli.py --seed "60,62,64,65,67" --temp 0.75→ C 大调上行后自然下行

6. 我的三个硬核习惯:让 RNN-LSTM 旋律生成从「能跑」到「拿高分」

带过 17 个班级的机器学习实践课,我总结出高分作业的共性不是模型多深,而是工程直觉够硬、验证手段够土、文档写得够懒人。下面这三条,是我自己写代码时雷打不动的习惯,也是我批改作业时一眼揪出「认真做了」和「Ctrl+C/V」的依据。

6.1 每次修改模型,必做「单步 debug」:用固定 seed 跑 3 个 token

LSTM 是黑匣子,但它的输入输出不是。我从不直接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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 15:04:22

电脑开机没有图标一文搞懂

3分钟搞定电脑开机没图标,附Win10/11完整示例 配置环境就卡半天?别急,电脑开机没图标这事儿,看似玄学,实则是系统资源加载或注册表配置的“小脾气”。很多搞公路工程的同行,野外作业多,笔记本常年风吹日晒,系统蓝屏或图标丢失是家常便饭。今天不整虚的,直接上干货。咱们结合嵌入式开发视角,把…

作者头像 李华
网站建设 2026/9/23 15:04:07

3分钟搞定打扮家环境,一文搞懂手写核心逻辑

3分钟搞定打扮家环境,一文搞懂手写核心逻辑 配置环境就卡半天,是不是你的常态?明明照着文档敲命令,报错却像天书,依赖版本冲突让你抓狂。别急,今天咱们不整虚的,直接上手,用 一文搞懂 的方式,把“打扮家”这个概念拆解到代码层面。…

作者头像 李华
网站建设 2026/9/23 15:04:06

2026最新苹果x和苹果8的区别图解原理面试突击

2026最新苹果x和苹果8的区别图解原理面试突击 报错一堆看不懂 StackTrace,屏幕上一行行红字像天书一样滚过,心跳瞬间加速。别慌,2026最新的面试趋势里,这种“看似无关”的硬件对比题,其实是考察你技术底层逻辑与产品思维的最佳切入点。很多候选人一听到“苹果x和苹果8的区别”,脑子里全是屏幕…

作者头像 李华
网站建设 2026/9/23 15:03:45

嵌合体开发踩坑实录:API变更下的性能优化实战

嵌合体开发踩坑实录:API变更下的性能优化实战 版本升级后 API 全变了,你的代码还在硬扛?别急着骂娘,先看看是不是掉进了“嵌合体”架构的陷阱。很多团队在追求高内聚低耦合时,为了兼容新旧接口,写出了一堆既不是纯微服务、也不是单体应用的“四不像”代码。这种 嵌合体 结构在初期看似灵活,实则成了…

作者头像 李华
网站建设 2026/9/23 15:03:33

3步搞定品牌之路手写实现,拒绝复制报错的最佳实践

3步搞定品牌之路手写实现,拒绝复制报错的最佳实践 代码从GitHub抄下来, npm install 完, npm run dev 一跑,满屏红字? 别慌,这种“看着能跑,实际崩盘”的尴尬,几乎每个刚入行的同学都经历过。…

作者头像 李华
网站建设 2026/9/23 15:03:28

爱奇艺之家报错一堆?一文搞懂Stack Trace排查与避坑指南

爱奇艺之家报错一堆?一文搞懂Stack Trace排查与避坑指南 凌晨三点,屏幕蓝光刺眼,你盯着IDE里那几百行红色的StackTrace,脑子像被浆糊糊住。每一行都像是天书, NullPointerException 混着 IndexOutOfBoundsException…

作者头像 李华