资讯详情

情绪识别+LSTM:智能音乐情绪生成器技术复盘

📅 2026/10/3 4:21:48 | 华诺云谱 👁 阅读
情绪识别+LSTM:智能音乐情绪生成器技术复盘
把一句话变成一段旋律这个念头我琢磨了很久。最近总算把“情绪识别 音乐生成”这条链路完整跑通做出来的东西我叫它智能音乐情绪生成器——你输入一句“加班到半夜的那种疲惫感”它就能生成一段低沉、缓慢、还带点压抑的MIDI旋律。这篇文章不是产品发布稿而是整个项目的技术复盘。我会从设计思路、核心代码、踩过的坑三个方面把每一步怎么取舍、怎么实现都讲清楚。适合想用AI辅助写歌的创作者、做短视频配乐的同学以及想了解LSTM这类序列模型怎么落地在具体项目上的开发者。整个方案基于Python实现核心就是情绪解析、音乐约束、LSTM生成三块没有用到非常重的框架照着我这个思路一台普通带GPU的机器就能跑起来。1. 这个项目到底在解决什么问题1.1 音乐情绪不是玄学背后有可计算的空间很多人觉得“音乐的情绪”是一种只可意会不可言传的东西但如果你接触过音乐理论就会知道它其实有很强的规律性。作曲家写一首悲伤的歌往往会用小调、慢速度、较低的旋律音区写一首欢快的歌则会用大调、快节奏、明亮的高音区。这不是某个人的个人偏好而是几百年来音乐实践沉淀下来的经验共识。心理学领域对情绪也有成熟的理论框架。我最常用的是Russell提出的Valence-Arousal二维情绪模型也就是用一个二维坐标来描述情绪状态横轴是Valence愉悦度从消极到积极纵轴是Arousal唤醒度从平静到兴奋。比如“悲伤”大致是低愉悦、低唤醒“愤怒”是低愉悦、高唤醒“开心”是高愉悦、高唤醒“放松”则是高愉悦、低唤醒。这个模型的价值在于它把情绪从模糊的文字标签变成了两个连续的数值而这正好可以跟音乐参数做映射。如果把音乐看作一个“可调参数的系统”那么调式、速度、音高范围、力度变化就分别对应着情绪空间的某个维度。这就像做菜辣度、咸度、甜度都是可调的你可以按配方组合出不同的风味。音乐情绪也一样有个可计算的“配方”。1.2 需求定位不是替代音乐人而是灵感引擎这个项目从一开始就不是奔着“AI替代作曲家”去的。以我目前的经验来看AI生成音乐的最大价值是当一个“无限供应灵感的草稿助手”。尤其是短视频配乐、游戏情景音乐、独立创作者做小样这种场景时间紧、预算低、又需要大量风格化的短旋律传统做法是翻素材库或者反复试听找感觉效率很低。所以我给这个生成器的定位就一句话输入情绪描述快速输出一段可编辑、可继续改写的MIDI旋律。MIDI不是最终音频它记录的是音符、时长、力度这些信息就像一个乐谱草稿。拿到MIDI之后你完全可以导入DAW比如Cubase、FL Studio、Logic里换音色、改和弦、重新编曲。这比直接生成一段不可编辑的音频要实用得多。对技术背景的要求其实不高。我自己不是音乐科班出身只是懂基础乐理所以在这个项目里我更倾向于“用规则补足知识盲区”乐理部分用映射规则解决旋律生成交给模型两者互补。如果你也像我一样对音乐理论半懂不懂对代码又没有特别深的研究这个项目的思路应该很适合参考。2. 技术选型与整体架构设计2.1 为什么输出用MIDI而不是直接生成波形刚开始我其实纠结过要不要直接生成音频毕竟现在用Diffusion模型生成音乐也已经有不少开源案例比如MusicGen这类模型直接输出音频。但对比之后我还是选了MIDI理由有三个。第一音频生成对算力和数据量的要求是MIDI方案的十倍以上。想生成流畅的音频波形至少要一个大模型预训练这基本超出了个人项目的资源范围。MIDI序列本质上就是数字序列用一个几十万参数的小模型就能训练一台普通游戏显卡的机器就能跑。第二MIDI的可编辑性极强。生成的音频改起来非常费劲而MIDI里每个音符都是独立的你可以随便移动、删改、改变力度和时值。对于创作者来说拿到一个能改的草稿比拿到一个“死”音频有价值得多。第三MIDI信息里自带音乐结构比如音符编号、小节位置这给后处理留了很多操作空间。可以方便地加和弦、加鼓点、改调性。所以在整个系统里我让模型的输出始终停留在音符序列层面音频转换只在最后一步做用合成器渲染成音频来试听就好。2.2 为什么生成模型选LSTM而不是Transformer在模型选型上我非常快地排除了Transformer选了LSTM。不是因为Transformer不好而是因为它不适合这个任务的前期迭代。音乐旋律是一种序列数据前后音符之间有很强的时序依赖。LSTM本来就是为这类任务设计的结构简单训练稳定在小规模数据上表现很好。而Transformer需要海量数据和更多训练技巧才能发挥威力它更适合长上下文的语义理解比如文本生成、大段音乐结构的建模。对于8到16小节的短旋律生成LSTM的序列建模能力完全够用而且部署和推理都轻量得多。我测试过一个对比同样用一万首MIDI转成的音符序列训练Transformer在40轮之后loss还有波动LSTM已经在稳步下降了。这不是Transformer本身不行而是数据量和训练参数没有到它的“舒适区间”。对一个个人项目来说LSTM是性价比更高的选择。如果你后续想做大段完整编曲、需要模型感知整个曲式结构再去考虑Transformer不迟。2.3 整体流程设计从一句话到MIDI文件整个系统是分层的流水线设计而不是一个端到端的黑盒模型。输入是文本输出是MIDI文件中间分成五步情绪解析从输入文本里提取出Valence和Arousal两个数值。音乐参数映射将情绪坐标映射为调式、BPM、音区、力度、和弦倾向等参数。种子序列初始化根据音乐参数生成一个初始的几个音符作为生成起点。模型采样LSTM模型根据已有音符和情绪向量逐个生成后续音符。后处理与MIDI输出修正音符范围、补上节奏型和对位和弦写入MIDI文件。分层设计的好处一是每一层都能单独调试。比如生成情绪不对你可以检查是情绪解析的问题还是映射规则的问题不用整个系统一起猜。二是每一层都可以被替换情绪解析以后可以换成大模型接口模型生成可以替换成采样效率更高的方法都不会影响其他模块。对个人项目来说这种“留后路”的设计方式非常重要因为需求总是在变。3. 核心实现拆解情绪到音符的每一步3.1 情绪识别层先让机器“听懂”一句话情绪识别最简单的路子是调大模型API但我在项目里没有这么做原因是不可控第一API响应有延迟本地还得处理网络问题第二不同模型输出的情绪标签格式不统一我还要再写一层解析。对一个生成本身的项目来说情绪识别只是一个前置模块不需要做得那么重用词典加规则就够了。我维护了一个小规模的情感词典每个词对应一组Valence和Arousal数值。当用户输入一句话时先做分词和词性过滤把名词、形容词、动词抽出来再去词典里匹配。命中的词的数值取平均就是整句话的情绪坐标。词典里没有的词就跳过如果全部没命中就用默认的“中性偏积极”兜底。emotion_lexicon { 疲惫: (-0.6, -0.4), 深夜: (-0.2, -0.5), 加班: (-0.3, 0.1), 希望: (0.7, 0.5), 激动: (0.6, 0.9), 平静: (0.5, -0.5), 愤怒: (-0.7, 0.8), ... } def parse_emotion(text): words tokenize_and_filter(text) vals [] for w in words: if w in emotion_lexicon: vals.append(emotion_lexicon[w]) if not vals: return (0.3, 0.2) # 中性偏积极兜底 v sum(x[0] for x in vals) / len(vals) a sum(x[1] for x in vals) / len(vals) return (v, a)这个方案很朴素但胜在速度快、结果稳定还完全离线。测试下来对简单情绪描述的准确率差不多有七成剩下的三成误差会在后面的映射层里做修正。如果你手头有精力也可以把情绪识别这部分替换成一个小型微调的文本分类模型效果会更好但成本也上来了。3.2 情绪向量到音乐参数的映射规则有了Valence和Arousal两个数值接下来就要把它们变成音乐参数。我从这条规则上花的时间最多因为它是整个项目里“乐理经验”最集中的地方。先说调式。Valence正值用大调负值用小调这个选择比较直接。再说BPM每分钟节拍数我把它跟Arousal挂钩Arousal是0.9的时候BPM干到140都没问题Arousal是-0.6的时候BPM就要压到70以下让人感觉慵懒、压抑。旋律音区则跟Valence和Arousal都有关系正面的、激烈的旋律倾向于走高音区负面的、冷静的旋律通常会落回中低音区。下面是我调试完比较稳定的一套映射逻辑情绪象限Valence范围Arousal范围调式BPM范围主音区MIDI编号高兴/兴奋0.3~1.00.5~1.0大调120~15072~84放松/温柔0.3~1.0-1.0~0.4大调70~9560~76悲伤/低落-1.0~0-1.0~0.2小调60~8050~68愤怒/紧张-1.0~00.5~1.0小调110~14060~80注意这里面BPM和音区都不是一个固定值而是一个范围。我实际用的是线性插值比如Arousal从0.4到1.0BPM就从95平滑升到150这样生成的情绪过渡更自然。直接给死数值会显得机械生成出来的音乐像两段完全不同风格的碎片拼在一起线性插值就好很多。def emotion_to_params(v, a): # 调式 mode major if v 0 else minor # BPM利用arousal线性映射到60~150区间 bpm int(60 (a 1) * 0.5 * 90) # 基础音区利用valence和arousal综合映射 base_note int(48 (v 1) * 0.5 * 24 (a 1) * 0.5 * 12) return {mode: mode, bpm: bpm, base_note: base_note, valence: v, arousal: a}这套映射的价值在于它把模型生成的方向“框”住了。模型再怎么自由发挥都不会跑出这个情绪范围太远。这也解决了一个很关键的问题AI生成音乐虽然“自由”但完全自由的结果往往是平庸甚至杂乱情绪感反而不强。加上映射约束等于给自由上了个轨道既可控又有变化。3.3 LSTM旋律生成模型数据来源我会在下一节细说先聊模型本身。输入是一段音符序列我用三种信息表示一个音符MIDI编号0到127、时值用多少分音符表示比如0.25是十六分音符0.5是八分音符1.0是四分音符、力度1到127。为了让模型知道当前生成应该走什么情绪路线我把情绪向量也拼到了输入里也就是说模型输入不只是一个音符序列而是“音符序列 情绪坐标”。这是整个设计里很关键的一步如果只训练音符序列模型学到的只是“常见音符接法”对情绪是无感知的拼接情绪向量之后模型才能学到“同样是C到G的进行在低唤醒和平静状态下更可能走向E在高唤醒状态下更可能走向A”这类情绪相关的规律。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Concatenate, Input def build_model(vocab_size, note_dim128, duration_dim8, vel_dim32, emotion_dim2): note_input Input(shape(seq_len,), namenote_seq) dur_input Input(shape(seq_len,), namedur_seq) vel_input Input(shape(seq_len,), namevel_seq) emo_input Input(shape(2,), nameemotion) note_emb Embedding(note_dim, 64)(note_input) dur_emb Embedding(duration_dim, 8)(dur_input) vel_emb Embedding(vel_dim, 16)(vel_input) x Concatenate()([note_emb, dur_emb, vel_emb]) lstm_out LSTM(128, return_sequencesFalse)(x) # 让情绪向量影响每一步生成 emo_repeat Dense(64, activationrelu)(emo_input) merged Concatenate()([lstm_out, emo_repeat]) out Dense(vocab_size, activationsoftmax)(merged) model Model(inputs[note_input, dur_input, vel_input, emo_input], outputsout) model.compile(optimizeradam, losscategorical_crossentropy) return model我试过把情绪向量重复成和LSTM每一步对齐的序列、用attention去动态调整权重但那样模型复杂度高了不少效果提升却有限。对个人项目来说这个简单的拼接方案够用了训练时间短、还容易调。3.4 节奏与和弦层让旋律不单薄光有单音旋律听起来还是干巴巴的。我在后处理阶段做了两件事一是生成了背景和弦二是叠加了一个简单的节奏型底鼓。和弦这里我用的是“情绪优先”的规则。用户在映射阶段已经得到了调式我根据调式生成一组和弦进行大调用I-V-vi-IV这种经典走向小调用i-VI-III-VII这种偏暗淡的走向。然后在模型生成旋律的同时把对应小节的根音和五音作为低音层铺下去。这一步不需要训练纯规则就能做得很好。def generate_chords(params): mode params[mode] if mode major: return [[C, G, Am, F], [C, G, Am, F], [F, C, G, Am]] else: return [[Am, F, C, G], [Am, F, Dm, E7]]底鼓节奏就简单了高Arousal就每拍踩一下低Arousal就只在正拍上给一个轻音。做这一步主要是为了让试听的时候“有感觉”毕竟一段光秃秃的单音旋律很难让人直观判断情绪对不对。加完这些东西你才能认真评估旋律本身写得怎么样。4. 完整实操流程从数据准备到生成出可听的旋律4.1 数据准备哪里找MIDI数据怎么标注情绪模型训练数据我用了两个来源一个是MAESTRO数据集主要收集钢琴演奏的MIDI一个是Lakh MIDI Dataset各种风格MIDI的合集。MAESTRO的质量很高很适合旋律生成Lakh数据量大但风格太杂很多文件质量参差不齐用之前需要清洗。清洗方式很简单把MIDI文件解析成音符序列去掉那种音符数过少比如不到32个音符的空文件去掉音轨数量特别多的、结构过于复杂的文件。解析用的是Python的music21库它可以把MIDI里的音符、时值、力度全部提取出来。提取之后统一格式每个音符转成一个三元组note, duration, velocity再转成上面的数值编码。情绪标注是整个数据准备里最难的部分。手动标一万首曲子不现实我用了粗标策略先按MIDI文件里的调号判断大调小调再按平均速度判断Arousal。大调且速度偏快归为高愉悦高唤醒小调且速度偏慢归为低愉悦低唤醒。这种粗标方式有误差但作为训练信号已经能给模型一个大概的方向。它好在能自动化一个小时就能标完全部文件。from music21 import converter, note def midi_to_sequence(midi_path): score converter.parse(midi_path) seq [] for part in score.parts: for n in part.flatten().notes: if isinstance(n, note.Note): seq.append((n.pitch.midi, n.duration.quarterLength, n.velocity)) return seq清洗完的数据长度大概是两万多个序列每个序列32个音符。按8比1比1切分成训练集、验证集、测试集然后把三元组编码成三个序列并行输入模型。4.2 训练配置与过程记录训练参数我调过几轮最终稳定下来的配置是序列长度32batch size64embedding维度64LSTM隐藏层128维dropout0.3学习率0.001训练100个epoch。训练在单张RTX 3060上跑了大概3个小时速度完全能接受。看loss曲线的过程很有意思。前10个epochloss从8.2快速降到6.1左右说明模型在快速学会“按概率接下一个音符”的基本规律。到30个epoch左右loss降到5.4附近出现了明显的平台期。这时候强行加大学习率会导致loss回弹我改用学习率衰减每10个epoch乘以0.95后期loss稳定在5.1左右不再下降了。训练完我做了简单的验证固定情绪向量为“开心”生成出来的旋律确实偏向大调、音符区间集中在高音区固定为“悲伤”旋律就明显进入小调、低音区节奏也慢了。这个阶段就说明情绪向量拼接起效了模型不是随机生成而是学会了情绪条件下的生成模式。4.3 生成MIDI并转成音频训练完之后生成就是纯推理过程。流程是先把情绪坐标映射成音乐参数再给一个种子音符比如C4四分音符然后让模型逐个预测下一个音符每预测一个就把新音符追加到序列尾部再作为输入继续预测。def generate_melody(model, params, seed_seq, emotion, num_steps64): seq seed_seq[:] for _ in range(num_steps): x prepare_input(seq) probs model.predict(x, verbose0)[0] note sample_from_probs(probs, temperature0.85) seq.append(note) return seq生成完的64个音符配合上后处理阶段生成的和弦与节奏型我直接用midiutil库把它们写成了MIDI文件。试听的时候还需要把MIDI渲染成音频我用的是FluidSynth加一个GM音色库一行命令就能转成wavfluidsynth -ni soundfont.sf2 output.mid -F output.wav听到第一版效果的时候还是有点小激动的输入“疲惫的深夜”生成的旋律确实是慢速、中低音区、小调而且居然还有一点连贯的呼吸感不是完全随机音符的堆砌。虽然跟专业作曲差得很远但作为“情绪草图”已经达到了项目目标。5. 常见问题与排查实录5.1 生成旋律总是循环重复怎么办这是我最先遇到的一类问题。模型学到的分布可能是“最安全的路径”所以生成出来的旋律经常出现321321321这种无限循环或者同一个动机反复出现。尤其是在训练数据里同一风格的曲子很多时模型很容易陷入这种局部最优。解决方式是引入温度采样。温度低于1时概率分布会更尖锐模型倾向于选最高概率的音符生成的旋律稳定但容易重复温度高于1时分布会变平坦低概率音符更容易被选中旋律更有变化但也更容易乱七八糟。我在测试后发现0.85到0.9之间是一个比较好的区间既有惊喜感又不至于完全失控。import numpy as np def sample_from_probs(probs, temperature0.85): logits np.log(probs 1e-8) / temperature exp_logits np.exp(logits - logits.max()) probs exp_logits / exp_logits.sum() return np.random.choice(len(probs), pprobs)顺便说一句生成的时候我还会保留前几步的随机种子。同样是“开心”前五个种子音符不同后面生成的整体走向也会有明显差异这让我能在同一个情绪条件下拿到多种备选旋律给用户更多选择。5.2 情绪特征在生成结果里几乎听不出来怎么办最早一版模型我没有把情绪向量拼进LSTM结果就是不管输入是“开心”还是“悲伤”生成结果听起来都差不多。原因很好理解模型只学到了统计上的“平均旋律分布”根本不知道当前生成任务的情绪条件自然每个情绪都往平均值上靠。后来我把情绪向量拼进LSTM输入情况好了不少但还不够明显。真正起决定性作用的是后处理层的强硬约束调式、BPM、音区都在情绪解析之后被钉死了模型生成的音符只能在这个范围内选择。这时候情绪利用的不是模型的“理解”而是规则层的“强制”。两者是一个互补关系。如果还是不明显我建议看看是不是输入文本的情绪解析阶段就偏了。比如“笑着哭”这种复杂情绪词典方案基本无能为力模型当然也听不出来。对这种表达最简单的思路是让用户直接拖动Valence和Arousal滑杆手动指定情绪坐标省去文本解析的中间环节可控性一下子就上来了。5.3 模型训练不收敛或loss降不下来有一次训练到20个epoch时loss一直在5.5左右徘徊怎么都不往下走。我一个一个排查数据量够不够够三万多条序列。学习率是不是太大也不是0.001是常规值。最后发现问题出在力度编码上。当时我把力度velocity直接做成整数输入没有做归一化这样模型输入分布的方差特别大梯度更新很不稳定。改成力度单独做embedding之后loss立刻就开始下降了。类似的坑还有时值编码一定不要直接用浮点数值塞进模型转成离散类别再embedding会稳得多。还有一个经验是序列长度别一开始就设64、128这种大值。序列越长LSTM的记忆负担越重训练难度越大。先用32长度把模型跑通再逐步加长这样调起来效率高很多。5.4 生成的旋律跨小节时会卡顿这是模型本身没学好小节结构导致的。LSTM只盯着音符序列并不知道“现在在第几小节”所以经常出现第4小节结尾和第5小节开头衔接生硬甚至出现一个音跨了两个小节的可笑情况。我调试了两步修正。第一步在输入里增加一个小节位置特征每个音符多带一个“当前小节序号对4取余”的通道这样模型至少能知道自己在小节的哪个位置。第二步后处理时对音符时值做裁剪强制不要超过小节末尾。这两步加起来衔接问题基本就消失了节奏感也好了很多。如果你要处理更长篇幅的音乐建议直接上Transformer结构给小节位置加绝对位置编码但那就是另外一个项目了。做这个项目最大的体会是情绪生成器最难的地方不是模型本身而是把音乐经验转成可计算的规则。模型负责创意规则负责方向两者缺一不可。我自己最后实际操作中保留的一个小习惯是把temperature参数固定在0.85左右BPM和音区先在映射规则里调出大概方向再来听模型生成的旋律细节这样调整效率最高。等哪天把情绪解析换成大模型或者直接做一个能拖滑杆的网页demo这个工具应该会更好玩到时候有新结果再跟大家汇报。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑