☰
情绪识别+LSTM:智能音乐情绪生成器技术复盘
2026/10/3 4:21:47 网站建设 项目流程

把一句话变成一段旋律,这个念头我琢磨了很久。最近总算把“情绪识别 + 音乐生成”这条链路完整跑通,做出来的东西我叫它智能音乐情绪生成器——你输入一句“加班到半夜的那种疲惫感”,它就能生成一段低沉、缓慢、还带点压抑的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文件,中间分成五步:

  1. 情绪解析:从输入文本里提取出Valence和Arousal两个数值。
  2. 音乐参数映射:将情绪坐标映射为调式、BPM、音区、力度、和弦倾向等参数。
  3. 种子序列初始化:根据音乐参数生成一个初始的几个音符,作为生成起点。
  4. 模型采样:LSTM模型根据已有音符和情绪向量,逐个生成后续音符。
  5. 后处理与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.0,BPM就从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_dim=128, duration_dim=8, vel_dim=32, emotion_dim=2): note_input = Input(shape=(seq_len,), name="note_seq") dur_input = Input(shape=(seq_len,), name="dur_seq") vel_input = Input(shape=(seq_len,), name="vel_seq") emo_input = Input(shape=(2,), name="emotion") 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_sequences=False)(x) # 让情绪向量影响每一步生成 emo_repeat = Dense(64, activation="relu")(emo_input) merged = Concatenate()([lstm_out, emo_repeat]) out = Dense(vocab_size, activation="softmax")(merged) model = Model(inputs=[note_input, dur_input, vel_input, emo_input], outputs=out) model.compile(optimizer="adam", loss="categorical_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 训练配置与过程记录

训练参数我调过几轮,最终稳定下来的配置是:序列长度32,batch size64,embedding维度64,LSTM隐藏层128维,dropout0.3,学习率0.001,训练100个epoch。训练在单张RTX 3060上跑了大概3个小时,速度完全能接受。

看loss曲线的过程很有意思。前10个epoch,loss从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_steps=64): seq = seed_seq[:] for _ in range(num_steps): x = prepare_input(seq) probs = model.predict(x, verbose=0)[0] note = sample_from_probs(probs, temperature=0.85) seq.append(note) return seq

生成完的64个音符,配合上后处理阶段生成的和弦与节奏型,我直接用midiutil库把它们写成了MIDI文件。试听的时候还需要把MIDI渲染成音频,我用的是FluidSynth加一个GM音色库,一行命令就能转成wav:

fluidsynth -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, temperature=0.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), p=probs)

顺便说一句,生成的时候我还会保留前几步的随机种子。同样是“开心”,前五个种子音符不同,后面生成的整体走向也会有明显差异,这让我能在同一个情绪条件下拿到多种备选旋律,给用户更多选择。

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,这个工具应该会更好玩,到时候有新结果再跟大家汇报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询