ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从情绪到旋律:用Python构建AI音乐情绪生成器全解析

从情绪到旋律:用Python构建AI音乐情绪生成器全解析 你有没有过这种时候——深夜加班耳机里的歌循环到麻木脑子里冒出一段旋律却怎么也抓不住。更恼火的是你想把它写成谱子才发现自己连五线谱都认不全。那段时间我一直在琢磨能不能搞一个工具输入一句“今天很烦躁”或者“雨天的下午有点忧郁”它就能帮我生成一段刚好配得上这种心情的旋律。于是就有了这个“智能音乐情绪生成器”AI负责理解情绪再用代码把情绪翻译成一段原创MIDI最后渲染成音频。整个过程不依赖在线API纯本地运行从数据标注、模型训练到生成推理全部由Python代码串联。如果你对AI生成音乐感兴趣或者单纯想做一个能把“情绪”变成“旋律”的小项目这篇文章应该能给你一整条可落地的技术路线。文中涉及的模型、代码、参数都是我在实际项目里反复调过的踩过的坑也会一一交代你可以直接把它当成一份完整的项目复盘来读。1. 一个听歌上头的夜晚项目起因与整体设计先说这个项目为什么值得做。市面上已经有不少音乐生成工具但大多数是“给一段输入还你一段风格统一但跟心情无关”的背景乐。我要的不是风格模板而是能跟随情绪变化的旋律。所以核心问题变成了机器怎么理解人的情绪又怎么把情绪翻译成几个音符的排列组合真正动手之前我先想清楚了三个必须回答的问题情绪在音乐里是怎么体现的快乐通常是快节奏、大调、音程跳跃多悲伤则相反——慢、小调、旋律下行。这是可以用数据去学出来的规律。机器怎么从音频里知道一首歌的情绪得先提取声学特征再用分类模型去映射到情绪标签。如何生成旋律生成式模型很多但个人项目里数据量不大LSTM是起步最快、最容易调通的选择。基于这三个问题我把整个系统设计成了一条五段式流水线文本或情绪标签输入 → 文本情感分析 → 情绪标签 → 条件LSTM旋律生成器 → MIDI渲染为音频。情绪分类负责“听”旋律生成负责“写”文本分析负责“读心”三者合成一个完整的“情绪翻译机”。架构听起来复杂拆开来看每一环其实都不算深。最花时间的是数据准备和两套模型的训练后面几章会重点展开。整体上我希望它能做到输入“想哭”输出类似肖邦夜曲气质的小调慢旋律输入“荷尔蒙爆棚”输出速度快、音域宽、节奏密集的段落。这个目标不算大但足够把一个技术闭环完整走通。2. 数据是燃料自建音乐情绪数据集的完整流程做AI的第一原则没有数据一切模型都是空中楼阁。音乐情绪生成器需要两套数据一套是带有情绪标注的音频用来训练“情绪分类器”另一套是MIDI旋律序列用来训练“旋律生成器”。两部分可以来自同一个数据源但处理方式完全不同。2.1 MIDI数据的来源与清洗我最终选择了MAESTRO数据集这是钢琴演奏的MIDI录音数据集包含超过200小时演奏版权相对干净结构也规范。如果你想做更现代的风格可以用Lakh MIDI Dataset但那里面的MIDI质量参差不齐清洗成本很高。个人项目优先用小而干净的MAESTRO。拿到MIDI文件后第一件事是清洗。原始MIDI可能有多条音轨还有鼓点和控制器信息这些对旋律生成是噪声。我用mido库读取MIDI只保留钢琴轨并降采样到固定速度同时过滤掉音量力度过低的事件。这里有一个关键决策我生成的是单音旋律线monophonic也就是同一时刻只有一个音符在响。这不是因为复调做不出来而是单音能让LSTM的序列长度和情绪特征都更容易控制。清洗后的MIDI需要转成序列。我用了一个简单的编码方式把每个音符映射为一个整数范围是0到127对应MIDI音高休止符单独用一个特殊标记。为了让模型能感知节奏我还在序列里插入了时值信息比如“音高时值”合并成一个token用两列数字表示。实际代码大致是这样import mido import numpy as np def midi_to_sequence(mid_file, max_len500): mid mido.MidiFile(mid_file) notes [] current_time 0 for msg in mid: if msg.type note_on and msg.velocity 0: pitch msg.note duration msg.time # 将音高和时值打包成一个整数pitch * 100 min(duration, 99) token pitch * 100 min(int(duration), 99) notes.append(token) elif msg.type note_off or (msg.type note_on and msg.velocity 0): continue if len(notes) max_len: notes notes[:max_len] else: notes notes [0] * (max_len - len(notes)) return np.array(notes, dtypenp.int64)注意这个编码方式很粗糙真实项目里我会用更复杂的“事件序列”编码但先跑通闭环比一开始就追求完美重要得多。2.2 音频特征提取与情绪标注旋律生成器只看MIDI序列情绪分类器则要处理真实音频。我从MAESTRO里把演奏渲染成WAV再用librosa提取特征。情绪识别领域最常用的特征组合是MFCC梅尔频率倒谱系数 Chroma色度特征 频谱对比度。我最终选用了Log-Mel Spectrogram作为输入因为它在语谱图上保留了足够的音乐信息而且可以作为二维图像喂给CNN。情绪标注是整条数据链里最费人力的环节。我参考了Russell的二维情绪模型用**Valence愉悦度和Arousal唤醒度**把情绪分成四个象限高愉悦高唤醒是“兴奋”低愉悦低唤醒是“悲伤”高愉悦低唤醒是“平静”低愉悦高唤醒是“愤怒”。我手工听了大概300首片段给每个片段打上四分类标签。过程很枯燥但也有个取巧的办法先用预训练模型粗标一遍再由人工修正可以省掉一半时间。音频特征提取的代码片段如下import librosa import numpy as np def extract_logmel(path, n_mels128, n_fft2048, hop_length512): y, sr librosa.load(path, sr22050, monoTrue) mel librosa.feature.melspectrogram(yy, srsr, n_melsn_mels, n_fftn_fft, hop_lengthhop_length) log_mel librosa.power_to_db(mel, refnp.max) # 固定长度比如取128帧约3秒 if log_mel.shape[1] 128: log_mel log_mel[:, :128] else: log_mel np.pad(log_mel, ((0, 0), (0, 128 - log_mel.shape[1])), modeconstant) return log_mel.reshape(128, 128, 1)这套特征和标签对齐后构成了情绪分类器的训练集。数据量不大但做四分类足够用了。3. 让机器听懂情绪音频情感分类模型从零训练音乐情绪分类本质上是一个音频分类任务。很多人会直接调用现成的识别接口我一开始也试过但很快放弃了。一是在线接口延迟不稳定二是返回的标签粒度太粗往往只有“happy/sad”两三个维度。自己训练模型不但可以完全控制类别还能离线运行把整个项目保持在一个自包含的Python环境里。3.1 模型结构我为什么选了一个“小号CNN”当时选项有两个方向一是用VGGish等预训练模型提取特征再接浅层分类器二是自己从零训练一个CNN。第一个方向代码量少但需要下载几百兆的预训练权重而且特征提取和情绪标签的适配度不一定好。第二个方向训练更可控数据量不大也不至于过拟合。我最后用了一个极简的CNN架构输入是128×128的Log-Mel图经过三层卷积池化展平后接两层全连接输出4类概率。效果能到82%左右的准确率对个人项目来说已经够用。模型定义代码如下import tensorflow as tf from tensorflow.keras import layers, models def build_emotion_net(input_shape(128, 128, 1), num_classes4): model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu), layers.GlobalAveragePooling2D(), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(num_classes, activationsoftmax) ]) return modelGlobalAveragePooling2D是我刻意选用的它比Flatten之后接全连接层参数更少对过拟合的压制效果很明显。训练时我把数据按6:2:2拆成训练、验证、测试类别不均衡问题通过给损失函数加权重解决。3.2 训练细节数据增强与类别权重音乐情绪数据和图像不一样加一点噪声、改变播放速度情绪并不会变但模型对鲁棒性的要求很高。我用librosa.effects.pitch_shift做了音高平移又叠加了高斯白噪声。这样训练出来的模型不会把某个录音室的特定噪声当成情绪特征。类别不均衡也是必然的。“悲伤”类样本特别多“愤怒”类特别少如果不处理模型会偷懒把所有样本都预测成悲伤。我给每类样本的loss乘了一个权重权重按样本数的倒数归一化。代码里就是class_weight参数history model.fit( train_spectrograms, train_labels, validation_data(val_spectrograms, val_labels), epochs60, batch_size32, class_weight{0: 1.0, 1: 1.4, 2: 0.8, 3: 1.8} # 具体数值按分布调整 )训练收敛后测试集准确率大约82%混淆矩阵里最容易混淆的是“平静”和“悲伤”——低唤醒状态下情绪边界本身就模糊这个结果可以接受。4. 用代码谱写旋律条件LSTM生成器的设计与调参情绪分类器只负责“听懂”音乐真正的主角是旋律生成器。我使用了条件LSTM它做的事情很简单给定一串历史音符和当前的情绪标签预测下一个音符的概率分布。生成的时候从分布里采样这样就得到了一段新旋律。4.1 旋律的表示把音高、时值变成一个token序列这个项目的生成器不需要复读机一样背谱而是学习音符之间的转移规律。所以我先把MIDI转成了整数序列每个token用pitch * 100 duration的方式压缩。这样音高和时值被编码在一个整数里模型只需输出一个词表概率。词表大小大概在7000左右由于没有特别大的模型我在Embedding层把每个token映射到128维向量。训练时把一个段落的全部token作为输入预测同序列的下一个token。核心代码from tensorflow.keras import layers, models def build_generator(vocab_size8000, n_steps128, emotion_dim8): note_input layers.Input(shape(n_steps,), namenotes) emotion_input layers.Input(shape(emotion_dim,), nameemotion) x layers.Embedding(vocab_size, 128)(note_input) x layers.LSTM(256, return_sequencesTrue)(x) x layers.LSTM(256)(x) # 将情绪向量拼接到LSTM输出上 combined layers.Concatenate()([x, emotion_input]) x layers.Dense(256, activationrelu)(combined) out layers.Dense(vocab_size, activationsoftmax)(x) model models.Model([note_input, emotion_input], out) return model这里的关键是“情绪怎么进去”。我用了拼接方式LSTM把整段序列编码成一个向量然后和情绪标签的嵌入向量拼接再接全连接输出。这个做法的好处是模型结构简单情绪信息对整个序列生成的方向都有影响而不是只在某一个时间步起作用。4.2 把情绪“告诉”生成器条件向量的设计情绪标签本身是离散的不能直接输入。最方便的做法是用Embedding层把“兴奋、悲伤、平静、愤怒”映射成8维向量然后作为条件输入。但我在实际实验中发现直接拼接embedding向量模型容易“忘记”情绪导致生成结果四个类都差不多。后来我把条件向量复制成跟序列长度一样与LSTM每个时间步的输出都拼接。这样情绪信息在每个时间步都在“提醒”生成器。具体实现是用RepeatVector在训练时耗费点内存但效果明显emotion_repeat layers.RepeatVector(n_steps)(emotion_input) # 这样在时间维度上扩展再和LSTM的中间输出拼接用这个办法之后“悲伤”类别生成的小调倾向明显增强音高均值也降低了不少。实践证明条件注入方式比模型结构本身更影响生成质量。4.3 生成过程的温度采样与情绪参数控制模型训练完后生成阶段需要决定从概率分布中挑哪个token。如果每次取概率最大的旋律会变成呆板的教科书式音阶。我用了经典的温度采样temperature samplingdef sample_with_temperature(logits, temperature1.0): logits logits / temperature exp_logits np.exp(logits - np.max(logits)) probs exp_logits / np.sum(exp_logits) return np.random.choice(len(probs), pprobs)温度小于1会增强确定性旋律更稳固大于1会增加跳跃听起来更有戏剧性。惊喜的是不同情绪天然适合不同的温度悲伤我喜欢用0.8让它稳定在小调的氛围里兴奋用1.1让音程跨度大一些有种不安分的感觉。除了温度我还根据情绪类别限定了初始音符的范围比如悲伤类从较低音区起步兴奋类从中高音区起步这算是一个便宜有效的先验约束。5. 集成输出从一句文字到一段成品音乐的全流程两套模型都训练好之后剩下的工作就是写一个可用的入口让用户输入情绪描述输出一段WAV音频。这块看着简单实际串起来时还是有几个容易卡壳的细节。5.1 文本输入怎么变成情绪标签我一开始图省事想用深度学习做文本情感分类但数据量不够。后来发现一个小巧的办法直接用情感词典的方式把输入文本和情绪词进行匹配。中文可以预先准备一份情绪词典英文可以直接用textblob库获取polarity然后映射到四个标签。def map_text_to_emotion(text): # 一个极简的规则映射够用就好 sad_words [哭, 累, 难过, 孤独, 忧伤] happy_words [开心, 兴奋, 爽, 幸福] calm_words [安静, 平静, 放松, 温柔] angry_words [愤怒, 生气, 烦, 暴躁] for word in sad_words: if word in text: return sad # ... return calm如果是含混合情绪的句子我建议先做分句分别打标签然后在生成阶段把情绪向量加权融合。这样可以做出“高唤醒低愉悦”这种更复杂的情绪组合但第一版你完全可以从四选一开始。5.2 MIDI生成、渲染与混音实践生成模型输出的token序列需要逆变换成MIDI音符再写进MIDI文件。我用mido或者midiutil都行但从模型输出到MIDI最容易出问题的是时值信息在逆变换时被丢掉。我在序列编码时把时值压在了个位数上所以恢复时要做pitch token // 100、duration token % 100。MIDI文件本身不是音频为了让用户直接能听我用fluidsynth加载一个音色库SoundFont把MIDI合成WAV。个人项目不需要昂贵硬件用免费的音色库就够了。合成代码import os import subprocess def midi_to_wav(midi_path, wav_path): subprocess.run([ fluidsynth, -ni, soundfont_path, midi_path, -F, wav_path ], checkTrue)到这里整个闭环已经打通一句话进去一条旋律出来还带着情绪特征的明显倾向。我还加了一个小工具函数让它循环生成多个候选然后通过情绪分类器对生成的音频做“反向验证”——如果生成的音乐被情绪分类器识别成了用户输入的目标情绪才算一次成功生成。这个设计帮我省了很多人工试听的精力。6. 实测效果与踩坑实录那些让人想砸电脑的问题项目做到这个程度最有价值的部分不是模型本身而是过程中趟过的一个个坑。我按崩溃程度从轻到重讲几个。6.1 训练LSTM时loss反复震荡第一次训练旋律生成器时loss在前20轮不停跳动怎么也降不下去。排查了很久发现问题是token序列里休止符占比太高导致大多数样本预测的都是同一个token梯度被带偏。解决方法是给序列做截断时权重采样确保每个训练序列都包含足够多的有效音符同时对停顿token在损失函数里降权。另外一个容易被忽略的点是Embedding尺寸。词表8000对个人项目来说已经不小Embedding维度128刚开始偏大模型有大量参数要学但数据量又跟不上。后来我降到64训练反而更稳。6.2 温度参数高时旋律像“磕了药”温度采样参数调到1.5以上旋律会变成无调性乱弹单个音符跳跃极大甚至出现两个连续音高距离超过两个八度的“外星音乐”。听感非常糟糕。我后来规定采样后的音符和上一个音符的音程差必须在某个范围内超了就重新采样。这属于规则后处理简单粗暴但极其有效。6.3 情绪条件“失效”的坑前面说过条件向量只拼接在最末端时情绪对生成的影响很小。但还有一个更隐蔽的问题数据本身。如果训练集里“悲伤”类MIDI本身调性不统一模型学不到稳定的模式。我后来重新清洗数据把悲伤类曲子统一成小调兴奋类统一成大调这才让条件真正产生可见差异。这份“每种情绪对应调式”的约束看起来像作弊但其实音乐理论就是无数创作者总结出来的规则AI学习它和人类学习它没什么两样。6.4 MIDI合成时音色和节奏“飘”fluidsynth默认音色库如果质量差生成的钢琴音色会带明显的混响和电声感和模型想表达的古典气质差距很大。我换了好几个免费SoundFont最后锁定了一个口碑不错的钢琴音色库。合成到WAV之后还有可能因为MIDI里的时值不精确导致节奏机械感太强建议生成后手动加一点随机化琶音或者用弹性效果器处理一下。7. 后续还能怎么玩从玩具到工具的扩展思路项目到现在已经能玩了但离真正的产品还有距离。如果继续做下去我会优先考虑这几个方向。首先是和弦与复调。现在只生成单音旋律听感单薄。可以在LSTM输出后加一个简单和弦生成规则根据调式自动配三和弦或者训练一个和弦生成模型。然后是风格迁移把古典钢琴转向电子乐其实只需要在序列编码里加入乐器和风格标记训练出一条可控的多风格生成路径。另外UI方面我可以写一个Web界面把情感分类、旋律生成、音频渲染都部署到后端让用户直接在浏览器里输入句子这样才是真正能分享给朋友的产品形态。如果追求更强生成能力可以考虑用Transformer替换LSTM效果上限会高不少但对数据和算力的需求也水涨船高。建立在现有数据规模上LSTM加一些规则约束反而更稳。整个项目做下来我最大的体会是AI音乐生成真正困难的地方不在模型而在数据和音乐知识的转化。数据清洗花了我接近三分之一的时间情绪标注又花了一周而模型训练和调参反而很快。如果你想复现这个项目建议先从小规模数据开始不要指望一次就把复调、情绪、风格全部塞进去。一点点抠慢慢把每个环节调顺你会看到输入的悲伤变成一段真正让人安静下来的旋律那种感觉还是挺奇妙的。
返回列表