PyTorch实战:用RNN从零开始实现周杰伦歌词生成

前言

窗外的麻雀 在电线杆上多嘴

你说这一句 很有夏天的感觉

周杰伦的歌词以其独特的意象和韵律,成为了一代人的青春记忆。你是否想过,有一天 AI 也能像"方文山"一样,写出充满"杰伦风"的歌词?

本文将带领大家进入有趣的文本生成领域,使用 PyTorch 框架和循环神经网络(RNN),一步步训练一个能够生成周杰伦歌词的 AI 模型。我们将从数据准备开始,到模型构建、训练,最终实现自动化的歌词创作。

本案例的核心步骤包括:

  1. 数据集准备:加载周杰伦歌词,并构建词表。
  2. 模型构建:搭建一个包含词嵌入层、RNN 层和全连接层的神经网络。
  3. 模型训练:编写训练函数,让模型学习歌词的模式。
  4. 文本生成:编写预测函数,用训练好的模型来"写"歌词。

一、 数据集介绍

我们本次任务使用的是一个精心收集的周杰伦歌词数据集。该数据集汇集了从他第一张专辑《Jay》到第十张专辑《跨时代》中的所有歌词,共计 5819 行文本。这为我们的模型学习周董的独特语言风格提供了丰富的素材。

数据集部分内容展示如下:

bash 复制代码
想要有直升机
想要和你飞到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每天在想想想想著你
这样的甜蜜
让我开始相信命运
感谢地心引力
让我碰到你
漂亮的让我面红的可爱女人
...

二、 导包

python 复制代码
import torch
import jieba
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.optim as optim
import time

三、 数据预处理:构建词表

神经网络无法直接处理文本,它需要我们将文字转换为数字。词表(Vocabulary) 就是连接文字世界和数字世界的桥梁。它的作用是为语料库中的每一个独立字(或词)分配一个独一无二的数字索引。

构建词表的整体流程非常直观:

  1. 读取所有文本数据:将 5819 行歌词加载到内存中。
  2. 分词与去重:对每一行歌词进行分词(这里我们按"字"为单位),然后统计所有不重复的字。
  3. 构建映射 :创建两个字典,一个是 字 -> 索引 的映射(word_to_index),另一个是 索引 -> 字 的映射(index_to_word),后者在生成文本时会用到。
python 复制代码
# 获取数据,并进行分词,构建词表
def build_vocab():
    # 数据集位置
    file_name = 'data/jaychou_lyrics.txt'
    # 分词结果存储位置
    # 唯一词列表
    unique_words = []
    # 每行文本分词列表
    all_words = []
    # 遍历数据集中的每一行文本
    for line in open(file_name, 'r', encoding='utf-8'):
        # 使用jieba分词,分割结果是一个列表
        words = jieba.lcut(line)
        # print(words)
        # 所有的分词结果存储到all_words,其中包含重复的词组
        all_words.append(words)
        # 遍历分词结果,去重后存储到unique_words
        for word in words:
            if word not in unique_words:
                unique_words.append(word)
    # 语料中词的数量
    word_count = len(unique_words)
    # 词到索引映射
    word_to_index = {word: idx for idx, word in enumerate(unique_words)}
    # 歌词文本用词表索引表示
    corpus_idx = []
    # 遍历每一行的分词结果
    for words in all_words:
        temp = []
        # 获取每一行的词,并获取相应的索引
        for word in words:
            temp.append(word_to_index[word])
        # 在每行词之间添加空格隔开
        temp.append(word_to_index[' '])
        # 获取当前文档中每个词对应的索引
        corpus_idx.extend(temp)
    return unique_words, word_to_index, word_count, corpus_idx


if __name__ == "__main__":
    # 获取数据
    unique_words, word_to_index, unique_word_count, corpus_idx = build_vocab()
    print("词的数量:\n",unique_word_count)
    print("去重后的词:\n",unique_words)
    print("每个词的索引:\n",word_to_index)
    print("当前文档中每个词对应的索引:\n",corpus_idx)

通过这三步,我们就完成了数据预处理的核心工作,为后续的模型训练做好了准备。


四、 构建数据集对象

我们在训练的时候,为了便于读取语料,并送入网络,所以我们会构建一个Dataset对象

python 复制代码
class LyricsDataset(torch.utils.data.Dataset):
    def __init__(self, corpus_idx, num_chars):
        # 文档数据中词的索引
        self.corpus_idx = corpus_idx
        # 每个句子中词的个数
        self.num_chars = num_chars
        # 文档数据中词的数量,不去重
        self.word_count = len(self.corpus_idx)
        # 句子数量
        self.number = self.word_count // self.num_chars

    # len(obj)时自动调用此方法
    def __len__(self):
        # 返回句子数量
        return self.number

    # obj[idx]时自动调用此方法
    def __getitem__(self, idx):
        # idx指词的索引,并将其修正索引值到文档的范围里面
        """
        我 爱你 中国 , 亲爱 的 母亲
        word_count: 7
        num_chars: 2 一个句子由num_chars个词组成
        word_count-num_chars-2: 7-2-1=4  -1:网络预测结果y在x上后移一个词取值-1
        idx=5->start=4
        """
        start = min(max(idx, 0), self.word_count - self.num_chars - 1)
        end = start + self.num_chars
        # 输入值
        x = self.corpus_idx[start: end]
        # 网络预测结果(目标值)
        y = self.corpus_idx[start + 1: end + 1]
        # 返回结果
        return torch.tensor(x), torch.tensor(y)


if __name__ == "__main__":
    # 获取数据
    unique_words, word_to_index, unique_word_count, corpus_idx = build_vocab()
    # 数据获取实例化
    dataset = LyricsDataset(corpus_idx, 5)
    # 查看句子数量
    print('句子数量:', len(dataset))
    # x, y = dataset.__getitem__(0)
    x, y = dataset[0]
    print("网络输入值:", x)
    print("目标值:", y)

五、 构建网络模型

我们的歌词生成模型将由三个核心组件构成,它们各司其职,共同完成从输入一个字到预测下一个字的任务。

  1. 词嵌入层 (Embedding Layer)

    • 作用:将输入的数字索引(来自我们的词表)转换为一个高维、稠密的向量(即词向量)。这个向量能够捕捉到字的语义信息。
  2. 循环网络层 (RNN Layer)

    • 作用:这是模型的核心。它负责处理输入的词向量序列,通过其内部的"记忆"机制(隐藏状态),提取并编码句子的上下文信息。简单来说,它能记住前面出现了哪些字,从而为预测下一个字提供依据。
  3. 全连接层 (Linear Layer)

    • 作用:作为模型的"输出头"。它接收来自 RNN 层的上下文特征向量,并将其映射到整个词表的大小。输出的向量中,每一个元素对应词表中一个字的得分,得分最高的字就是模型预测的下一个字。
python 复制代码
# 模型构建
class TextGenerator(nn.Module):
    def __init__(self, unique_word_count):
        super(TextGenerator, self).__init__()
        # 初始化词嵌入层: 语料中词的数量, 词向量的维度为128
        self.ebd = nn.Embedding(unique_word_count, 128)
        # 循环网络层: 词向量维度128, 隐藏向量维度256, 网络层数1
        self.rnn = nn.RNN(128, 256, 1)
        # 输出层: 特征向量维度256与隐藏向量维度相同, 词表中词的个数
        self.out = nn.Linear(256, unique_word_count)

    def forward(self, inputs, hidden):
        # 输出维度: (batch, seq_len, 词向量维度128)
        # batch:句子数量
        # seq_len: 句子长度, 每个句子由多少个词 词数量
        embed = self.ebd(inputs)
        # rnn层x的表示形式为(seq_len, batch, 词向量维度128)
        # output的表示形式与输入x类似,为(seq_len, batch, 词向量维度256)
        # 前后的hidden形状要一样, 所以DataLoader加载器的batch数要能被整数
        output, hidden = self.rnn(embed.transpose(0, 1), hidden)
        # 全连接层输入二维数据, 词数量*词维度
        # 输入维度: (seq_len*batch, 词向量维度256) 
        # 输出维度: (seq_len*batch, 语料中词的数量)
        # output: 每个词的分值分布,后续结合softmax输出概率分布
        output = self.out(output.reshape(shape=(-1, output.shape[-1])))
        # 网络输出结果
        return output, hidden

    def init_hidden(self, bs):
        # 隐藏层的初始化:[网络层数, batch, 隐藏层向量维度]
        return torch.zeros(1, bs, 256)


if __name__ == "__main__":
    # 获取数据
    unique_words, word_to_index, unique_word_count, corpus_idx = build_vocab()
    model = TextGenerator(unique_word_count)
    for named, parameter in model.named_parameters():
        print(named)
        print(parameter)

这三个组件串联起来,形成了一个端到端的文本生成模型。


六、 模型训练

准备好数据和模型后,我们就可以开始最关键的"炼丹"环节------模型训练。训练的目标是让模型的预测结果尽可能地接近真实歌词的下一个字。

训练流程遵循深度学习的标准范式:

  1. 数据迭代 :从歌词数据中构建 (输入序列, 目标字) 的数据对。例如,对于歌词"天青色等烟雨",我们可以构建出 ("天青色等烟", "雨") 这样的训练样本。
  2. 前向传播:将输入序列送入我们构建的网络模型,得到对下一个字的预测概率分布。
  3. 计算损失 :文本生成本质上是一个分类问题------在所有可能的字中,预测哪一个是正确答案。因此,我们使用多分类交叉熵损失函数(Cross-Entropy Loss) 来衡量模型预测与真实目标字之间的差距。
  4. 反向传播与参数更新 :计算损失函数关于模型参数的梯度,并使用优化器(如 Adam)来更新模型的权重,使得损失越来越小。
  5. 模型保存 :经过多轮训练,当模型性能稳定后,我们使用 torch.save 将训练好的模型权重保存到磁盘,以便后续预测使用。
python 复制代码
def train():
    # 构建词典
    unique_words, word_to_index, unique_word_count, corpus_idx = build_vocab()
    # 数据集 LyricsDataset对象,并实现了 __getitem__ 方法
    lyrics = LyricsDataset(corpus_idx=corpus_idx, num_chars=32)
    # 查看句子数量
    # print(lyrics.number)
    # 初始化模型
    model = TextGenerator(unique_word_count)
    # 数据加载器 DataLoader对象,并将lyrics dataset对象传递给它
    lyrics_dataloader = DataLoader(lyrics, shuffle=True, batch_size=5)
    # 损失函数
    criterion = nn.CrossEntropyLoss()
    # 优化方法
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    # 训练轮数
    epoch = 10
    for epoch_idx in range(epoch):
        # 训练时间
        start = time.time()
        iter_num = 0  # 迭代次数
        # 训练损失
        total_loss = 0.0
        # 遍历数据集 DataLoader 会在后台调用 dataset.__getitem__(index) 来获取每个样本的数据和标签,并将它们组合成一个 batch
        for x, y in lyrics_dataloader:
            # 隐藏状态的初始化
            hidden = model.init_hidden(bs=5)
            # 模型计算
            output, hidden = model(x, hidden)
            # 计算损失
            # y形状为(batch, seq_len), 需要转换成一维向量->160个词的下标索引
            # output形状为(seq_len, batch, 词向量维度)
            # 需要先将y进行维度交换(和output保持一致)再改变形状
            y = torch.transpose(y, 0, 1).reshape(shape=(-1,))
            loss = criterion(output, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            iter_num += 1  # 迭代次数加1
            total_loss += loss.item()
        # 打印训练信息
        print('epoch %3s loss: %.5f time %.2f' % (epoch_idx + 1, total_loss / iter_num, time.time() - start))
    # 模型存储
    torch.save(model.state_dict(), 'model/lyrics_model_%d.pth' % epoch)


if __name__ == "__main__":
    train()

这个过程会重复进行多轮(Epochs),直到模型在歌词数据上达到满意的拟合效果。


七、 歌词生成:构建预测函数

当模型训练完成后,最激动人心的时刻到来了!我们将编写一个预测函数,让模型真正地开始"创作"。

生成歌词的过程是一个自回归(Auto-regressive) 的循环过程:

  1. 加载模型:从磁盘加载我们之前保存的训练好的模型,并将其设置为评估模式。
  2. 指定开头:给定一个或多个起始字,比如 "窗外"。
  3. 首次预测:将起始字(序列)送入模型,得到对下一个字的预测。我们选择概率最高的那个字作为第一个生成字。
  4. 循环生成
    • 最新生成的字追加到当前序列的末尾。
    • 将这个新的、更长的序列再次作为输入,送入模型,预测下一个字。
    • 重复这个过程。
python 复制代码
def predict(start_word, sentence_length):
    # 构建词典
    unique_words, word_to_index, unique_word_count, _ = build_vocab()
    # 构建模型
    model = TextGenerator(unique_word_count)
    # 加载参数
    model.load_state_dict(torch.load('model/lyrics_model_10.pth'))
    # 隐藏状态
    hidden = model.init_hidden(bs=1)
    # 将起始词转换为索引
    word_idx = word_to_index[start_word]
    # 产生的词的索引存放位置
    generate_sentence = [word_idx]
    # 遍历到句子长度,获取每一个词
    for _ in range(sentence_length):
        # 模型预测
        output, hidden = model(torch.tensor([[word_idx]]), hidden)
        # 获取预测结果
        word_idx = torch.argmax(output)
        generate_sentence.append(word_idx)
        # 根据产生的索引获取对应的词,并进行打印
    for idx in generate_sentence:
        print(unique_words[idx], end='')


if __name__ == '__main__':
    # 调用预测函数
    predict('分手', 50)

这个"用自己的输出来喂养自己"的循环,会一直持续下去,直到生成我们指定长度的歌词。


八、总结

本文通过一个完整的"周杰伦歌词生成"案例,详细介绍了如何使用 RNN 来处理自然语言处理中的文本生成任务。我们从最基础的数据处理和词表构建,到核心的 RNN 模型设计,再到训练和最终的生成,走完了整个流程。

通过这个项目,我们不仅学会了 RNN 的实际应用,也亲身体验了让 AI 进行内容创作的魅力。快动手试试,看看你的 AI 能写出怎样"哎哟,不错哦"的歌词吧!

九、参考资料

本文核心知识点均提炼自 B 站黑马程序员 的精品课程,老师讲得非常详细,大家一定要去支持!

相关推荐
HyperAI超神经2 小时前
在线教程丨微软开源3D生成模型TRELLIS.2,3秒生成高分辨率的全纹理资产
人工智能·深度学习·机器学习·3d
IT阳晨。2 小时前
【CNN卷积神经网络(吴恩达)】目标检测学习笔记
深度学习·目标检测·cnn
gsgbgxp2 小时前
安装库是优先用conda还是pip
深度学习·ubuntu·conda·pip
有Li2 小时前
LoViT:用于手术阶段识别的长视频Transformer/文献速递-基于人工智能的医学影像技术
论文阅读·人工智能·深度学习·文献·医学生
查无此人byebye2 小时前
手写Multi-Head Attention多头注意力机制,Pytorch实现与原理详解
人工智能·pytorch·python·深度学习·transformer
Wilber的技术分享2 小时前
【Transformer原理详解2】Decoder结构解析、Decoder-Only结构中的Decoder
人工智能·笔记·深度学习·llm·transformer
赋创小助手3 小时前
Maia 200 技术拆解:微软云端 AI 推理加速器的设计取舍
服务器·人工智能·科技·深度学习·神经网络·microsoft·自然语言处理
高洁013 小时前
知识图谱在装备领域应用场景
python·深度学习·机器学习·数据挖掘·知识图谱
咚咚王者3 小时前
人工智能之核心技术 深度学习 第三章 卷积神经网络(CNN)
人工智能·深度学习·cnn