1. RNN歌词生成项目概述
在自然语言处理领域,文本生成一直是个有趣且实用的研究方向。作为一名长期从事AI项目开发的工程师,我发现使用循环神经网络(RNN)来生成歌词是个绝佳的练手项目。这个项目不仅能帮助我们理解RNN处理序列数据的核心机制,还能产出让人眼前一亮的创意成果。
歌词生成本质上属于序列预测问题 - 给定前面的词序列,预测下一个最可能出现的词。与传统分类任务不同,这里每个预测步骤的输出又会成为下一个步骤的输入,这种自回归特性使得RNN这类具有记忆能力的网络结构特别适合此类任务。
特别提示:在实际项目中,中文歌词生成比英文更具挑战性,主要难点在于中文需要先进行分词处理,且语言的意境表达更为抽象。这也是为什么我选择周杰伦歌词作为数据集 - 他的歌词既有丰富的意象组合,又能保持一定的语法结构。
2. 数据集构建全流程
2.1 原始数据收集与清洗
我从周杰伦从第一张专辑《Jay》到第十张专辑《跨时代》中收集了所有歌词文本,共5819行。原始数据需要经过以下处理步骤:
- 编码统一:确保所有文本都是UTF-8编码
- 特殊符号处理:去除无意义的标点符号,但保留换行符等有结构意义的符号
- 空白字符处理:将连续空格合并为单个空格
- 非歌词内容剔除:去除"[间奏]"、"(合唱)"等标注性内容
python复制# 原始数据预处理示例
import re
def clean_text(text):
# 移除方括号及其中内容
text = re.sub(r'\[.*?\]', '', text)
# 移除圆括号及其中内容
text = re.sub(r'\(.*?\)', '', text)
# 合并连续空格
text = re.sub(r'\s+', ' ', text)
return text.strip()
2.2 中文分词处理
中文不像英文有天然的空格分隔,因此需要专门的分词处理。我对比了多种分词工具后选择了jieba:
python复制import jieba
# 加载自定义词典(可加入周杰伦特有词汇)
jieba.load_userdict('custom_dict.txt')
# 示例分词
line = "天青色等烟雨而我在等你"
words = jieba.lcut(line) # ['天青色', '等', '烟雨', '而', '我', '在', '等', '你']
实际项目中发现,对于歌词这种艺术性文本,严格按词典分词反而会破坏意境。我的经验是适当调整jieba的cut_all参数,并在自定义词典中加入歌手特有的组合词(如"天青色")。
2.3 词表构建技术细节
词表构建是NLP任务的基础环节,这里分享几个关键实现要点:
- 去重统计:遍历所有分词结果,收集唯一词列表
- 索引分配:为每个词分配唯一ID,通常按词频排序
- 特殊标记:需要预留[UNK]、[PAD]等特殊标记的索引
- 词频过滤:可设置阈值过滤低频词,减少词表大小
python复制def build_vocab(text_lines, min_count=3):
word_counts = {}
# 第一遍遍历:统计词频
for line in text_lines:
words = jieba.lcut(line)
for word in words:
word_counts[word] = word_counts.get(word, 0) + 1
# 过滤低频词
filtered_words = [w for w in word_counts if word_counts[w] >= min_count]
# 构建词表
word_to_idx = {'[PAD]':0, '[UNK]':1}
for idx, word in enumerate(filtered_words, start=2):
word_to_idx[word] = idx
return word_to_idx
在我的实现中,最终得到了5703个唯一词。这个规模对于RNN模型来说稍大,但保留了周杰伦歌词的语言特色。
3. 数据集类设计与实现
3.1 PyTorch Dataset类封装
为了高效加载数据,我们需要自定义Dataset类。关键设计考虑包括:
- 序列长度:统一截断或填充到固定长度
- 滑动窗口:使用滑动窗口生成训练样本
- 批处理:支持shuffle和并行加载
python复制class LyricsDataset(torch.utils.data.Dataset):
def __init__(self, corpus, seq_length=32):
self.corpus = corpus
self.seq_length = seq_length
self.total_words = len(corpus)
def __len__(self):
return self.total_words - self.seq_length
def __getitem__(self, idx):
inputs = self.corpus[idx:idx+self.seq_length]
targets = self.corpus[idx+1:idx+self.seq_length+1]
return torch.tensor(inputs), torch.tensor(targets)
3.2 数据加载技巧
在实际使用中,我发现以下几个技巧能显著提升数据加载效率:
- 预加载:小数据集可以全部加载到内存
- 内存映射:对于大数据集使用np.memmap
- 缓存机制:对预处理结果进行磁盘缓存
- 并行加载:设置DataLoader的num_workers参数
python复制# 示例DataLoader配置
dataloader = DataLoader(
dataset,
batch_size=64,
shuffle=True,
num_workers=4,
pin_memory=True # 加速GPU传输
)
4. 模型训练中的数据处理技巧
4.1 序列批处理策略
RNN处理变长序列时,需要特殊的批处理方式。我的经验是:
- 动态填充:同一批次内填充到相同长度
- 掩码机制:忽略填充部分对损失的影响
- 打包序列:使用pack_padded_sequence优化计算
python复制from torch.nn.utils.rnn import pad_sequence
def collate_fn(batch):
inputs, targets = zip(*batch)
inputs = pad_sequence(inputs, batch_first=True)
targets = pad_sequence(targets, batch_first=True)
return inputs, targets
4.2 温度采样(Temperature Sampling)
在文本生成中,直接选择概率最高的词会导致结果过于保守。我引入了温度参数来调整输出的随机性:
python复制def sample_with_temperature(logits, temperature=1.0):
logits = logits / temperature
probs = torch.softmax(logits, dim=-1)
return torch.multinomial(probs, 1)
不同温度值的效果对比:
- 高温(>1.0):更多样化但可能不连贯
- 低温(<1.0):更保守但更流畅
- 适中(0.5-0.8):平衡创意和连贯性
5. 实际项目中的经验总结
5.1 常见问题与解决方案
问题1:生成结果重复
- 原因:模型陷入局部最优
- 解决方案:增加dropout、使用beam search、引入惩罚项
问题2:生僻词过多
- 原因:词频分布不均衡
- 解决方案:对词频取对数作为采样权重
问题3:上下文不连贯
- 原因:序列长度不足
- 解决方案:增大RNN的hidden size或使用注意力机制
5.2 性能优化记录
在我的RTX 3090上,原始实现每个epoch需要约3分钟。通过以下优化降至45秒:
- 混合精度训练:使用torch.cuda.amp
- 梯度累积:小batch size下模拟大batch
- 序列打包:减少padding的计算浪费
- 缓存隐藏状态:避免重复计算
python复制# 混合精度训练示例
scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
output, hidden = model(inputs, hidden)
loss = criterion(output, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
5.3 创意生成的进阶技巧
要让生成的歌词更有"周杰伦风格",我尝试了以下方法:
- 风格向量:在embedding层添加风格控制向量
- 多任务学习:同时预测下一个词和段落结束标志
- 外部知识注入:使用关键词生成然后扩展
- 后处理过滤:基于押韵规则筛选结果
python复制# 简单押韵检测
def is_rhyme(word1, word2):
vowels = ['a', 'e', 'i', 'o', 'u']
last_vowel1 = [c for c in word1 if c in vowels][-1]
last_vowel2 = [c for c in word2 if c in vowels][-1]
return last_vowel1 == last_vowel2
这个项目最让我惊喜的是,当模型训练到第30个epoch左右时,开始产生一些有意境的词句组合,比如"月光下谁在弹奏/寂寞的钢琴手",虽然语法不算完美,但已经展现出创作潜力。后续我计划尝试将RNN替换为Transformer,并引入更大的华语流行歌词数据集,应该能得到更专业的结果。
