ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于TensorFlow与LSTM的歌词生成:从数据清洗到温度采样实战

基于TensorFlow与LSTM的歌词生成:从数据清洗到温度采样实战 简介这是一份基于TensorFlow构建LSTM神经网络生成歌词的Python项目资源面向对自然语言处理、循环神经网络感兴趣的开发者尤其适合希望从零跑通一个文本生成小项目的中级学习者。项目名为encore.ai核心思路是读取指定艺术家的全部歌词让模型学习其用词习惯、押韵方式和换行结构输入一个种子词即可生成带有该风格的新歌词理论上支持从猫王到当代歌手的任意艺术家。资源压缩包共22个文件其中14个Python脚本涵盖数据清洗、歌词抓取、词频统计、模型定义、训练与保存等完整流程另有2个AI设计源文件、2张示意图、LICENSE与README说明文档包体仅3.33MB轻量易用。代码按data_wrangling和code等目录组织方便对照流程阅读。目前已有323人学习下载。通过阅读源码和README读者可快速掌握LSTM文本生成的核心方法包括数据预处理、模型搭建、采样生成与结果保存也能了解到处理长文本时的踩坑思路很适合作为入门深度学习和文本生成的实践参考。1. 从艺术家风格到可训练的字级序列Kanye West 那句话的续写结果Never I admit I cant do I see a drug som...乍看像乱码细看却有节奏感短词连用、押韵位置、口语化的缩略方式都带着原作者的痕迹。这就是 encore.ai 这套源码在做的事——用 TensorFlow 搭一个 LSTM 网络把某个歌手全部歌词当成序列数据训练让模型学会的不是背歌词而是单词与换行符之间的转移概率。这套代码的价值在于它把文本生成拆成了完整可复现的工程链路数据抓取与清洗、字级序列编码、模型训练、采样生成每一环都有独立脚本对应适合刚接触循环神经网络、想搞懂文本生成全流程的 Python 开发者也适合已经在做 NLP、想快速搭一个风格化生成 demo 的工程师。接下来从数据管线讲起因为大部分 LSTM 项目跑不出效果问题往往不出在模型而出在喂进去的文本太脏。2. 歌词语料管线抓取、清洗、合并与词频统计2.1 抓取脚本为什么拆成三层data_wrangling 目录下按功能拆了三个脚本get_all_artists.py 拿艺术家列表get_all_songs.py 拿某位艺术家的歌曲列表get_song_lyrics.py 拿每首歌的歌词正文。这个拆分不是多余——歌词类站点普遍有反爬和分页限制拆开后可以做到哪一层失败就重试哪一层不用每次从头跑。常见的做法是这三层共用一批请求函数统一处理超时和重试。伪代码大致如下# data_wrangling/fetch_utils.py示意 import requests, time def fetch_json(url, retries3, backoff2.0): for attempt in range(retries): try: resp requests.get(url, timeout10) resp.raise_for_status() return resp.json() except requests.RequestException: time.sleep(backoff * (attempt 1)) return None这个函数里的 retries 控制重试次数backoff 控制指数退避的基数。歌词站对频繁请求很敏感固定间隔重试容易触发封禁指数退避能明显降低失败率。三层脚本各维护一个本地 JSON 或文本缓存爬取结果落盘后即使后续清洗逻辑写错也不需要重新请求网络。2.2 quick_clean.py 的清洗规则歌词文本比普通语料脏得多有段落标记有合唱标注有演唱者名字穿插还有各种 unicode 引号和全角空格。quick_clean.py 做的主要是这几件事清洗项处理方式示例结构标记删除整行[Chorus]、[Verse 1]、(Hook)空白符统一为单个\n连续空行压缩为一行引号与撇号转为 ASCII’、‘→非歌词行按关键词过滤含 lyrics、album 的行清洗函数的核心逻辑一般长这样import re def clean_lyrics(raw_text: str) - str: lines raw_text.splitlines() kept [] for line in lines: line line.strip() if not line: continue if re.match(r^[\[\(], line): continue line line.replace(’, ).replace(‘, ) kept.append(line) return \n.join(kept)这里的关键是正则^[\[\(]它匹配以左方括号或左圆括号开头的行把[Chorus]这类标记整行丢掉。保留换行符非常重要LSTM 会把\n当作普通字符来学模型后续才能学会在合适的位置断行。清洗后的文本建议用utf-8落盘Windows 下要注意默认编码问题写入时显式指定encodingutf-8否则后面训练读入时会出现乱码。2.3 combine_artists.py 合并语料与 word_count.py 质量检查单首歌词喂给 LSTM 没有意义模型需要的是某个艺术家全部作品的拼接。combine_artists.py 把同一歌手的歌词文件合并成一个大的纯文本文件中间用\n\n分隔歌曲。合并时有两处容易踩坑一是文件遍历顺序不稳定导致每次训练语料顺序不同建议先对文件列表sorted()二是歌曲间要加分隔符否则上一首歌的结尾词会和下一首歌的开头词连在一起生成时会产出一堆跨歌的奇怪搭配。word_count.py 是质量检查工具统计语料里每个词的出现次数。它有两个用途第一跑完数据管线后先看一下高频词是不是歌手常用词如果高频词是 lyrics、chorus 这类清理残留说明清洗规则漏了东西第二统计词表规模为后续建模提供参考。运行方式很简单python data_wrangling/word_count.py --input data/kanye_west.txt --top 50输出里如果出现大量无意义的标点、URL、或 embed 这类词就要回到清洗步骤补规则。词表大小同时决定了模型输出层的维度这个数字需要在训练前确认好。2.4 怎么确认语料管线输出正确数据管线跑完后不要去读整个语料文件而是用几条命令做冒烟测试wc -l data/kanye_west.txt head -n 20 data/kanye_west.txt tail -n 20 data/kanye_west.txt我一般还会检查两点文件是否以换行符结尾以及歌词之间的分隔是否一致。很多 LSTM 训练 loss 不下降原因就是语料文件末尾缺少换行符导致最后一段歌词的最后一个字的上下文被截断。另外项目根目录下的 test.txt 是拿来快速验证链路用的改模型阶段没必要每次都加载全量语料用小文件先跑通再上全量能省下大量调试时间。3. LSTMModel.py 的模型设计Embedding、LSTM 单元与序列输出3.1 字符级还是词级LSTM 建模单位的选择encore.ai 选择的是字符级模型。原因很实际歌词里大量出现缩写、口语拼写和自造词比如输入示例里的bout、re-united词级模型遇到这些词只能丢到 OOV词表外处理字符级模型就没有这个问题——它能学会字母组合的规律甚至可以发明新词。字符级训练的代价是序列长度变长同样的文本量按字符切分后要比按词切分多出 3~5 倍的训练步数。但歌词文本本身不长单首歌也就几百个字符这个代价完全可以接受。数据处理上字符级和常见的 LSTM 时间序列预测套路很相似都是把原始序列切成长度为seq_length的窗口前seq_length-1个字符做输入后seq_length-1个字符做目标。区别在于时间序列输入是连续数值这里要把字符先映射成整数索引再经过 Embedding 层变成稠密向量。3.2 模型结构Embedding、LSTM 层与输出层LSTMModel.py 定义的结构大致可以拆成三层Embedding 层把字符 ID 映射为向量LSTM 层学习序列依赖Dense 层输出每个字符位置上的概率分布。用 TensorFlow 2 的 Keras 接口重写的话核心代码如下import tensorflow as tf class LSTMModel(tf.keras.Model): def __init__(self, vocab_size, embedding_dim128, lstm_units256, num_layers2, dropout_rate0.3): super().__init__() self.embedding tf.keras.layers.Embedding(vocab_size, embedding_dim) self.lstm_layers [ tf.keras.layers.LSTM(lstm_units, return_sequencesTrue, dropoutdropout_rate) for _ in range(num_layers) ] self.dense tf.keras.layers.Dense(vocab_size) def call(self, inputs, statesNone): x self.embedding(inputs) for lstm in self.lstm_layers: x lstm(x) return self.dense(x)参数上embedding_dim 一般取 128 或 256歌词词表小取 128 足够lstm_units 取 256 是起步值语料量达到几 MB 时可以加到 512。num_layers2 能捕捉到更高层的语言结构但层数再多在小语料上容易过拟合。dropout_rate 只对层与层之间的输入生效LSTM 内部状态上默认不做 dropout。注意return_sequencesTrue这个参数它让 LSTM 在每一个时间步都输出隐藏状态而不是只在最后一个时间步输出。因为歌词生成是逐字预测每个输入字符都要对应一个输出预测这个参数必须打开。3.3 LSTM 遗忘门如何影响歌词生成LSTM 的核心是细胞状态它在序列中传递长期信息。遗忘门是其中一个门控结构它接收上一时刻的隐藏状态h_{t-1}和当前输入x_t作为输入经过 sigmoid 激活后输出一个 0 到 1 之间的值决定上一时刻细胞状态中有多少信息要被保留。放到歌词场景里这一步直接决定了模型能不能学会跨行押韵。比如 midnight 这个词在一句结尾出现后面跟了换行符到了下一句的前几个词模型需要记得前面的韵脚才能选择 light或 sight这类相近发音的词。这个记忆就是通过遗忘门保留在细胞状态里的。如果遗忘门学到的权重接近 0模型会变得健忘生成的歌词每一句内部通顺但句与句之间没有任何主题关联。这也解释了为什么单层 LSTM 生成歌词容易飘——它只能记住很短距离的上下文。LSTM 里的另一个特点是它对梯度传递友好门控机制缓解了循环神经网络里的梯度消失问题。实际训练中能看到的现象是字符级 LSTM 在前几个 epoch 内 loss 下降很快后续变慢这正是因为短距离的字符搭配比如常见的字母组合很快就被学到而长距离的语义关联需要更多轮次才能被遗忘门调整到位。3.4 老项目迁移到 TensorFlow 2 的注意点encore.ai 的原始代码如果是在 TensorFlow 1.x 下写的直接在新环境跑会遇到不少兼容问题。TensorFlow 2 里已经没有tf.nn.rnn_cell.LSTMCell这种用法替代方案是tf.keras.layers.LSTMCell配合tf.keras.layers.RNN封装或者直接使用tf.keras.layers.LSTM。# TensorFlow 2 中保留自定义状态传递的写法 lstm_cell tf.keras.layers.LSTMCell(256) rnn_layer tf.keras.layers.RNN(lstm_cell, return_sequencesTrue)用LSTMCell加RNN封装的好处是可以手动控制初始状态做生成推理时可以用已经跑完种子文本的状态作为起点。这个能力在训练时不需要但在 save_samples.py 这类生成脚本里是必需的。安装环境建议直接用 Anaconda 建独立环境Python 3.8 配 TensorFlow 2.10 是兼容性比较稳的组合不要在系统 Python 里直接装避免和已有项目产生依赖冲突。TensorFlow 与 PyTorch 在文本生成上的取舍这两年讨论很多单就这个项目来说保持 TensorFlow 路线不用换框架代码改动量更小。4. runner.py 训练循环与采样生成过程4.1 训练循环的组织方式runner.py 负责把数据读取、模型前向、损失计算、梯度更新串起来。训练阶段的输入不是单条文本而是把整个语料按batch_size切块再在每个块内按seq_length窗口滑动。这样每个 batch 里包含多个样本梯度更新更稳定。核心训练逻辑通常类似下面这段# runner.py 训练核心简化 optimizer tf.keras.optimizers.Adam(learning_rate0.002) loss_fn tf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue) tf.function def train_step(input_seq, target_seq): with tf.GradientTape() as tape: logits model(input_seq) loss loss_fn(target_seq, logits) grads tape.gradient(loss, model.trainable_variables) grads, _ tf.clip_by_global_norm(grads, 5.0) # 梯度裁剪 optimizer.apply_gradients(zip(grads, model.trainable_variables)) return loss for epoch in range(epochs): for batch_input, batch_target in dataset: loss train_step(batch_input, batch_target)learning_rate 取 0.002 是 Adam 优化器下的常见起点loss 不降时可下调到 0.001 或 0.0005。from_logitsTrue表示 Dense 层输出的是未经过 softmax 的 logits交叉熵损失内部会先做 softmax不要在 Dense 层额外加 softmax 激活否则梯度会不稳定。tf.clip_by_global_norm做梯度裁剪clip 值取 5.0。字符级 LSTM 在长序列上容易出现梯度爆炸表现是 loss 突然变成 NaN裁剪能有效避免这个问题。数据集构造时data_reader.py 负责把文本切分为(input, target)二元组输入是text[i: iseq_length]目标是text[i1: iseq_length1]两者整体错位一个字符。4.2 save_samples.py 的温度采样逻辑训练到一定程度后保存模型参数只是第一步真正看到效果要靠采样生成。采样不能直接取概率最大的字符那样生成的文本会陷入重复循环。save_samples.py 里用的通常是带温度系数的随机采样def sample_with_temperature(logits, temperature0.8): logits logits / temperature probs tf.nn.softmax(logits, axis-1) return tf.random.categorical(tf.math.log(probs), 1)温度参数控制概率分布的锐利程度。temperature 越小概率大的字符被选中的概率越高输出越保守temperature 越大分布越平缓输出越随机。project 里默认值一般取 0.5 到 0.9 之间具体取决于训练收敛程度。模型没训练充分时用 0.8 以上的温度会产出大量乱字符。tf.random.categorical接收的是 log 概率而不是原始概率所以代码里先用tf.math.log(probs)转换。它返回的是形状为(batch_size, 1)的整数索引对应选中字符在词表中的位置再用词表反向映射还原成字符。4.3 种子词怎么影响生成结果生成的第一步是给定种子词或短语比如 Never I admit。这个种子词不会直接出现在最终输出里它的作用是让模型的 LSTM 状态从零初始化过渡到进入状态。具体做法是把种子词按同样的字符映射方式转换成整数序列输入模型做一次前向但不从中采样只拿最后一个时间步的隐藏状态和细胞状态然后从这个状态开始逐字采样并更新状态。seed_text Never I admit seed_ids [char_to_idx[c] for c in seed_text] state None for c in seed_ids[:-1]: input_tensor tf.reshape(c, (1, 1)) output, state model(input_tensor, state) # 从 seed_ids 最后一个字符开始正式采样种子词越长生成结果受种子词的约束越强。如果种子词里包含原歌手歌词里的罕见写法模型会倾向延续那种风格但也更容易让生成结果带上明显的拼接感。种子词长度为 5 到 10 个字符比较合适过短时模型基本是从歌手整体风格均值开始生成不够有针对性。4.4 训练与生成怎么衔接验证训练过程中每完成一个 epoch建议用当前模型跑一遍 save_samples.py把生成样本保存到独立文件。不要只盯着 loss 数值看loss 下降不代表生成质量好。我通常在 epoch 1、5、10、20 各存一份输出对比能看到从乱码到有单词、到有语法结构、再到出现风格化表达的完整过程。如果某个 epoch 开始出现整段重复或退化成只输出几个高频字符说明模型过拟合了应该降低层数、加大 dropout 或提前停止而不是继续训练。这个判断在训练曲线里看不出来只能在采样文件里发现。5. 温度和种子词控制生成歌词风格的边界生成阶段最有价值的动手实验是固定同一个 checkpoint只调温度参数观察输出变化。下表是一个典型现象参考适用于绝大多数歌词语料温度区间典型现象适用场景0.2 ~ 0.4高频重复常出现整行原歌词调试模型是否收敛0.5 ~ 0.8结构稳定、有押韵感默认生成区间0.9 ~ 1.2语法松散、新词增多寻找灵感时使用大于 1.2基本是乱码字符随机组合无参考价值温度参数需要配合生成长度动态调整。生成长度超过 200 字时即使温度 0.6 也容易在后面陷入重复循环此时两个做法比较有效一是把种子词改短让模型在更开放的条件下重选方向二是在采样时对最近 20 个字符做 n-gram 去重如果下一个候选字符会让最近的 3-gram 重复出现就把它从采样列表中临时剔除from collections import Counter recent generated_text[-20:] def masked_logits(logits, recent, vocab, n3): for i, token in enumerate(vocab): gram f{recent[-(n-1):]}{token} if recent.count(gram) 2: logits[i] -1e9 return logits这一步把重复 n-gram 对应的 logits 设为极小值softmax 后这些字符的采样概率近似为 0。代价是偶尔会牺牲一个合法候选但对打破循环很有效。验证模型是否学到风格可以用 word_count.py 统计生成文本的高频词和目标歌手真实歌词的高频词列表做覆盖度对比覆盖率达到 60% 以上说明风格迁移基本成立。本文还有配套的精品资源点击获取
返回列表