目录
今天我们将深入分析一个基于PyTorch的RNN歌词生成模型,这个模型能够学习周杰伦歌词的风格并自动生成新的歌词。我们将从数据预处理开始,逐步讲解模型构建、训练过程和预测生成的每个环节。
1. 数据预处理模块
数据预处理是任何NLP任务的基础,下面我们来看看这个模型是如何处理歌词数据的。
1.1 读取和分词处理
def read_data():
file_name = 'data/jaychou_lyrics.txt'
all_words = [] # 所有单词
unique_words = [] # 去重的单词
这部分代码首先定义了数据读取函数,使用jieba分词对中文歌词进行分词处理。中文分词是将连续的中文文本切分成单个词语的过程,对于后续的文本生成任务至关重要。
代码通过遍历文本的每一行,使用jieba.lcut(line)进行分词,然后将所有词语收集到all_words列表中,同时维护一个去重后的词汇表unique_words。这种处理方式与常见的NLP预处理流程一致,都是先将文本转换为词语序列。
1.2 构建词汇表和数值化映射
word_to_idx = {word: idx for idx, word in enumerate(unique_words)}
corpus_idx = []
for words in all_words:
tmp = []
for word in words:
tmp.append(word_to_idx[word])
tmp.append(word_to_idx[' '])
corpus_idx.extend(tmp)
这里创建了一个单词到索引的映射字典word_to_idx,然后将文本中的每个词语替换为对应的数字索引。这种数值化表示是神经网络处理文本数据的基础,类似于词嵌入的初步处理。
值得注意的是,代码在每个句子后面添加了空格对应的索引,这是为了在生成歌词时保持词语之间的分隔,使生成的文本更可读。
2. 数据集类设计
2.1 自定义Dataset类
class LyricsDataset(Dataset):
def __init__(self, corpus_idx, seq_len):
self.corpus_idx = corpus_idx
self.seq_len = seq_len
self.word_count = len(corpus_idx)
self.num = self.word_count // self.seq_len
这个类继承自PyTorch的Dataset类,用于处理序列数据的加载。seq_len参数决定了模型一次处理的序列长度,这对于RNN模型非常重要,因为它影响了模型能够捕捉的上下文信息长度。
2.2 数据获取方法
def __getitem__(self, idx):
idx = min(max(0,idx),self.word_count - self.seq_len - 1)
x = self.corpus_idx[idx:idx + self.seq_len]
y = self.corpus_idx[idx + 1:idx + self.seq_len + 1]
return torch.tensor(x), torch.tensor(y)
__getitem__方法是Dataset类的核心,它返回输入序列x和目标序列y。这里采用了一种常见的语言模型训练策略:将输入序列向后偏移一个位置作为目标序列。例如,如果输入是["我","爱","你"],那么目标就是["爱","你","<下一个词>"]。这样设计的目的是让模型学习根据前面的词语预测下一个词语。
3. RNN模型架构
3.1 模型初始化
class LyricsModel(nn.Module):
def __init__(self, word_count):
super(LyricsModel, self).__init__()
self.embadding = nn.Embedding(num_embeddings=word_count, embedding_dim=128)
self.rnn = nn.RNN(input_size=128, hidden_size=256, num_layers=1)
self.out = nn.Linear(in_features=256, out_features=word_count)
这个RNN模型包含三个主要组件:
-
嵌入层(Embedding Layer):将离散的词语索引转换为密集的向量表示。这种表示能够捕捉词语之间的语义关系。
-
RNN层:处理序列信息,捕捉文本中的时序依赖关系。这里使用隐藏层大小为256的单层RNN。
-
输出层(Linear Layer):将RNN的输出映射回词汇表空间,生成每个可能词语的概率分布。
3.2 前向传播过程
def forward(self, input, hidden):
input = self.embadding(input)
output, hidden = self.rnn(input.transpose(0, 1), hidden)
output = self.out(output.squeeze())
return output, hidden
前向传播过程展示了数据在模型中的流动:
-
输入序列通过嵌入层转换为向量序列
-
RNN层处理序列并返回输出和隐藏状态
-
输出层将RNN输出转换为词汇表上的概率分布
这里的转置操作input.transpose(0, 1)是因为PyTorch的RNN层期望的输入维度是(seq_len, batch_size, input_size)。
4. 模型训练策略
4.1 训练循环设置
def train_model():
my_dataset = LyricsDataset(corpus_idx=corpus_idx, seq_len=32)
my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
model = LyricsModel(word_count=word_count)
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3,betas=(0.9, 0.999))
criterion = nn.CrossEntropyLoss()
训练设置采用了以下关键组件:
-
DataLoader:批量处理数据,支持数据打乱以提高训练效果
-
Adam优化器:自适应学习率优化算法,适合处理稀疏梯度
-
交叉熵损失函数:多分类任务的标准损失函数
4.2 训练过程细节
for epoch in range(epochs):
for x, y in my_dataloader:
hidden = model.init_hidden(batch_size=1)
output, hidden = model(x, hidden)
loss = criterion(output, y.reshape(-1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
训练循环遵循标准的PyTorch训练模式:前向传播、损失计算、反向传播和参数更新。每个epoch中,模型会遍历整个数据集,逐步调整参数以最小化预测误差。
值得注意的是,这里使用hidden = model.init_hidden(batch_size=1)在每个批次开始时初始化RNN的隐藏状态,这确保了不同批次之间的独立性。
5. 歌词生成与预测
5.1 预测初始化
def predict_model(start_word, sequence_length):
unique_words, word_to_index, word_count, corpus_idx = read_data()
model = LyricsModel(word_count)
model.load_state_dict(torch.load('model/lyric_model_3.pth'))
model.eval()
预测阶段首先加载训练好的模型参数,并通过model.eval()将模型设置为评估模式。这一步很重要,因为它会关闭dropout和batch normalization等训练特有的层,确保预测结果的一致性。
5.2 自回归生成过程
result = [idx]
for _ in range(sequence_length):
output, hidden = model(torch.tensor([[idx]]), hidden)
idx = torch.argmax(output, dim=-1)
result.append(idx.item())
这是歌词生成的核心部分,采用自回归生成方式:
-
从起始词语开始,将其输入模型得到下一个词语的概率分布
-
选择概率最高的词语作为下一个词语(这里使用贪心策略)
-
将新生成的词语作为下一步的输入,重复这个过程
这种逐步生成的方式允许模型基于之前生成的所有上下文来创造新的内容,类似于人类创作的过程。
6. 模型优化与改进建议
虽然这个基础RNN模型已经能够实现歌词生成,但还有多种方式可以进一步提升其性能和生成质量:
6.1 模型架构改进
-
使用LSTM或GRU:解决传统RNN的梯度消失问题,捕捉更长距离的依赖关系
-
增加模型深度:使用多层RNN可以学习更复杂的文本特征
-
注意力机制:让模型能够关注输入序列中更相关的部分
6.2 训练策略优化
-
批量训练:当前代码使用批量大小为1,增加批量大小可以加速训练并提高稳定性
-
学习率调度:动态调整学习率可以帮助模型更好地收敛
-
早停机制:防止模型过拟合训练数据
6.3 生成质量提升
-
束搜索(Beam Search):代替贪心策略,考虑多个可能序列,生成更优结果
-
温度采样(Temperature Sampling):控制生成过程中的随机性,平衡创造性和连贯性
-
重复惩罚:避免模型陷入重复循环的生成模式
完整代码
import torch
import torch.nn as nn
import jieba
import numpy as np
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
import time
# TODO 数据处理
def read_data():
# 导入歌词文件
file_name = 'data/jaychou_lyrics.txt'
# 创建一个空列表,用于存储歌词
all_words = [] # 所有单词
unique_words = [] # 去重的单词
# 读取文件,遍历文本
for line in open(file_name, 'r', encoding='utf-8'):
# 分词
words = jieba.lcut(line)
all_words.append(words) # 添加所有单词
for word in words:
if word not in unique_words:
unique_words.append(word)
# print("所有单词数:", len(all_words))
# print("去重后单词数:", len(unique_words))
word_count = len(unique_words)
# 创建单词索引
word_to_idx = {word: idx for idx, word in enumerate(unique_words)}
# print("单词索引:", word_to_idx)
corpus_idx = []
# 将句子进行数值化
for words in all_words:
tmp = []
for word in words:
# 把token转化成数字
tmp.append(word_to_idx[word])
# 添加空格,以便区分句子
tmp.append(word_to_idx[' '])
# 把tmp添加到列表中,得到一个一维的大列表
corpus_idx.extend(tmp)
# print("句子数值化:", corpus_idx)
return unique_words, word_to_idx, word_count, corpus_idx
def test_read_data():
unique_words, word_to_idx, word_count, corpus_idx = read_data()
print("unique_words-->", unique_words)
print("word_to_idx-->", word_to_idx)
print("word_count-->", word_count)
print("corpus_idx-->", corpus_idx)
class LyricsDataset(Dataset):
def __init__(self, corpus_idx, seq_len):
# 语料
self.corpus_idx = corpus_idx
# 序列长度
self.seq_len = seq_len
# 单词数
self.word_count = len(corpus_idx)
# 句子数
self.num = self.word_count // self.seq_len
def __len__(self):
return self.num
def __getitem__(self, idx):
# print("idx-->", idx)
# 将idx限制在[0, word_count - seq_len - 1]区间内
# 防止起始位置加上序列长度后超出词汇总数范围
idx = min(max(0,idx),self.word_count - self.seq_len - 1)
# print("idx-->", idx)
# x\y
x = self.corpus_idx[idx:idx + self.seq_len]
y = self.corpus_idx[idx + 1:idx + self.seq_len + 1]
return torch.tensor(x), torch.tensor(y)
def test_LyricsDataset():
unique_words, word_to_idx, word_count, corpus_idx = read_data()
# corpus_idx: 语料库索引,用于指定使用的歌词语料库
# seq_len: 序列长度,指定每个训练样本的序列长度为5
my_dataset = LyricsDataset(corpus_idx=corpus_idx, seq_len=5)
print("my_dataset-->", my_dataset[0])
# TODO 网络模型构建
class LyricsModel(nn.Module):
def __init__(self, word_count):
# word_count:词表大小,用于embadding层和out层
super(LyricsModel, self).__init__()
# 构建词嵌入层
# num_embeddings: 词表大小; embedding_dim: 词嵌入维度
self.embadding = nn.Embedding(num_embeddings=word_count, embedding_dim=128)
# 构建RNN层,参数:输入维度、隐藏维度、层数
self.rnn = nn.RNN(input_size=128, hidden_size=256, num_layers=1)
# 构建输出层,输出维度为词表大小
self.out = nn.Linear(in_features=256, out_features=word_count)
def forward(self, input, hidden):
# 打印input的形状,用于构建函数逻辑
# print("input.shape-->", input.shape)
# 将输入序列通过嵌入层转换为词向量表示,即流入embadding层
input = self.embadding(input)
# 将嵌入后的输入传递给RNN层,获取输出和隐藏状态
# 输入需要转置以匹配RNN期望的维度格式
output, hidden = self.rnn(input.transpose(0, 1), hidden)
# 通过全连接层将RNN输出映射到目标词汇空间,预测5703个类别结果
# 线性层一般接收二维输入
# squeeze()方法将输出的维度从[batch_size, seq_len, word_count]变为[batch_size, word_count]
output = self.out(output.squeeze())
return output, hidden
def init_hidden(self, batch_size):
return torch.zeros(1, batch_size, 256)
def test_LyricsModel():
unique_words, word_to_idx, word_count, corpus_idx = read_data()
# 把数据转换为dataset
my_dataset = LyricsDataset(corpus_idx=corpus_idx, seq_len=32)
# 把数据转换成dataloader
my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
model = LyricsModel(word_count=word_count)
for x, y in my_dataloader:
hidden = model.init_hidden(batch_size=1)
# print("hidden-->", hidden.shape) # [1, 1, 256],一个样本一个隐藏状态,一个隐藏状态有256个embadding
# print("x-->", x.shape) # [1, 32],一个样本有32个token
# print("y-->", y.shape) # [1, 32],一个样本有32个token
output, hidden = model(x, hidden)
# print("output-->", output.shape) # [32, 5703],32个token的预测结果5703个类别结果
# TODO 模型训练
def train_model():
unique_words, word_to_idx, word_count, corpus_idx = read_data()
# 创建歌词数据集对象,用于处理和加载歌词文本数据
# corpus_idx: 语料库索引,用于指定使用的歌词语料库
# seq_len: 序列长度,设置每个训练样本的序列长度为32
my_dataset = LyricsDataset(corpus_idx=corpus_idx, seq_len=32)
# dataset: 要加载的数据集对象
# batch_size: 批次大小,设置为1表示每次处理一个样本
# shuffle: 是否打乱数据,设置为True表示每个epoch随机打乱数据顺序
my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
# word_count: 词汇表大小,用于确定模型输出层的神经元数量
model = LyricsModel(word_count=word_count)
# 创建Adam优化器,用于模型参数优化
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3,betas=(0.9, 0.999))
# 创建交叉熵损失函数,用于多分类任务的损失计算
criterion = nn.CrossEntropyLoss()
# 设置训练轮数
epochs = 3
total_loss = 0.0
total_num = 0
start_time = time.time()
for epoch in range(epochs):
for x, y in my_dataloader:
hidden = model.init_hidden(batch_size=1)
output, hidden = model(x, hidden)
# print("x-->", x.shape) # [1, 32]
# print("y-->", y.shape) # [1, 32]
#
# print("hidden-->", hidden.shape) # [1, 1, 256]
# print("output-->", output.shape) # [32, 5703]
# y.reshape(-1)将标签张量展平为一维,确保与output的维度匹配
loss = criterion(output, y.reshape(-1))
# print("loss-->", loss)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_num += len(y)
total_loss += loss.item()
if total_num % 300 == 0:
print("当前轮次:", epoch+1,"当前损失:", total_loss / total_num,"当前耗时:", time.time() - start_time)
torch.save(model.state_dict(), 'model/lyric_model_%d.pth' % (epoch + 1))
# TODO 模型预测
def predict_model(start_word, sequence_length):
# start_word:起始词,模型基于这个词来续写歌词
# sequence_length:需要生成多少的歌词
# 获取数据
unique_words, word_to_index, word_count, corpus_idx = read_data()
# 初始化模型
model = LyricsModel(word_count)
# 加载参数
model.load_state_dict(torch.load('model/lyric_model_3.pth'))
# 把模型设置为评估模式
model.eval()
# 把start_word转换为idx
idx = word_to_index[start_word]
# print("idx-->", idx)
# 准备hidden数据
hidden = model.init_hidden(batch_size=1)
# 定义一个列表,用于存储目前词语
result = [idx]
# 生成歌词
for _ in range(sequence_length):
# 把start_word送给模型
# print(torch.tensor(idx).unsqueeze(0).unsqueeze(0).shape) # [1,1]
# print(torch.tensor(idx).reshape(1, 1).shape)
# idx -> idx.shape:[]
# [[idx]] -> shape:[1, 1]
output, hidden = model(torch.tensor([[idx]]), hidden)
# print("output-->", output.shape)
# 从output中找到下标索引最大的值
idx = torch.argmax(output, dim=-1)
# 把idx添加到预测结果列表中
result.append(idx.item())
# 打印result的结果
print("result-->", result)
# 把数字解码成token(词语)
for j in result:
print(unique_words[j], end='')
if __name__ == '__main__':
# read_data()
# test_read_data()
# test_LyricsDataset()
# test_LyricsModel()
# train_model()
predict_model('我', 150)
数据集 jaychou_lyrics.txt
想要有直升机
想要和你飞到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每天在想想想想著你
这样的甜蜜
让我开始相信命运
感谢地心引力
让我碰到你
漂亮的让我面红的可爱女人
温柔的让我心疼的可爱女人
透明的让我感动的可爱女人
坏坏的让我疯狂的可爱女人
坏坏的让我疯狂的可爱女人
漂亮的让我面红的可爱女人
温柔的让我心疼的可爱女人
透明的让我感动的可爱女人
坏坏的让我疯狂的可爱女人
坏坏的让我疯狂的可爱女人
想要有直升机
想要和你飞到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每天在想想想想著你
这样的甜蜜
让

最低0.47元/天 解锁文章
1309

被折叠的 条评论
为什么被折叠?



