跳转至

4 案例-文本生成

学习目标

  • 掌握文本生成模型构建流程

文本生成任务是一种常见的自然语言处理任务,输入一个开始词能够预测出后面的词序列。本案例将会使用循环神经网络来实现周杰伦歌词生成任务。

数据集如下:

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

数据集共有 5819 行。

1. 构词词典

我们在进行自然语言处理任务之前,首要做的就是就是构建词表。所谓的词表就是将语料进行分词,然后给每一个词分配一个唯一的编号,便于我们送入词嵌入层。

最终,我们的词典主要包含了:

  1. word_to_index: 存储了词到编号的映射
  2. index_to_word: 存储了编号到词的映射

一般构建词表的流程如下:

  1. 语料清洗, 去除不相关的内容
  2. 对语料进行分词
  3. 构建词表

接下来, 我们对周杰伦歌词的语料数据按照上面的步骤构建词表。

# 构建词典
def build_vocab():

    file_name = 'data/jaychou_lyrics.txt'


    # 1. 清洗文本
    clean_sentences = []
    for line in open(file_name, 'r'):

        line = line.replace('〖韩语Rap译文〗','')
        # 去除中文、英文、数字、部分标点符号外的其他字符
        line = re.sub(r'[^\u4e00-\u9fa5 a-zA-Z0-9!?,]', '', line)
        # 连续空格替换成1个
        line = re.sub(r'[ ]{2,}', '', line)
        # 去除两侧空格、换行
        line = line.strip()
        # 去除单字的行
        if len(line) <= 1:
            continue

        # 去除重复行
        if line not in clean_sentences:
            clean_sentences.append(line)

    # 2. 预料分词
    index_to_word, all_sentences = [], []

    for line in clean_sentences:
        words = jieba.lcut(line)
        all_sentences.append(words)
        for word in words:
            if word not in index_to_word:
                index_to_word.append(word)

    # 词到索引映射
    word_to_index = {word: idx for idx, word in enumerate(index_to_word)}
    # 词的数量
    word_count = len(index_to_word)
    # 句子索引表示
    corpus_idx = []
    for sentence in all_sentences:
        temp = []
        for word in sentence:
            temp.append(word_to_index[word])
        # 在每行歌词之间添加空格隔开
        temp.append(word_to_index[' '])
        corpus_idx.extend(temp)


    return index_to_word, word_to_index, word_count, corpus_idx

def test01():

    index_to_word, word_to_index, word_count, corpus_idx = build_vocab()
    print(word_count)
    print(index_to_word)
    print(word_to_index)
    print(corpus_idx)

2. 构建数据集对象

我们在训练的时候,为了便于读取语料,并送入网络,所以我们会构建一个 Dataset 对象,并使用该对象构建 DataLoader 对象,然后对 DataLoader 对象进行迭代可以获取语料,并将其送入网络。

class LyricsDataset:

    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


    def __len__(self):
        return self.number

    def __getitem__(self, idx):

        # 修正索引值到: [0, self.word_count - 1]
        start = min(max(idx, 0),  self.word_count - self.num_chars - 2)

        x = self.corpus_idx[start: start + self.num_chars]
        y = self.corpus_idx[start + 1: start + 1 + self.num_chars]

        return torch.tensor(x), torch.tensor(y)


def test02():

    _, _, _, corpus_idx = build_vocab()
    lyrics = LyricsDataset(corpus_idx, 5)
    lyrics_dataloader = DataLoader(lyrics, shuffle=False, batch_size=1)

    for x, y in lyrics_dataloader:
        print('x:', x)
        print('y:', y)
        break

3. 构建网络模型

我们用于实现《歌词生成》的网络模型,主要包含了三个层:

  1. 词嵌入层: 用于将语料转换为词向量
  2. 循环网络层: 提取句子语义
  3. 全连接层: 输出对词典中每个词的预测概率

我们前面学习了 Dropout 层,它具有正则化作用,所以在我们的网络层中,我们会对词嵌入层、循环网络层的输出结果进行 Dropout 计算。

示例代码如下:

class TextGenerator(nn.Module):

    def __init__(self, vocab_size):
        super(TextGenerator, self).__init__()
        # 初始化词嵌入层
        self.ebd = nn.Embedding(vocab_size, 128)
        # 循环网络层
        self.rnn = nn.RNN(128, 128, 1)
        # 输出层
        self.out = nn.Linear(128, vocab_size)

    def forward(self, inputs, hidden):

        # 输出维度: (1, 5, 128)
        embed = self.ebd(inputs)

        # 正则化层
        embed = F.dropout(embed, p=0.2)

        # 修改维度: (5, 1, 128)
        output, hidden = self.rnn(embed.transpose(0, 1), hidden)

        # 正则化层
        embed = F.dropout(output, p=0.2)

        # 输入维度: (5, 128)
        # 输出维度: (5, 5682)
        output = self.out(output.squeeze())

        return output, hidden

    def init_hidden(self):
        return torch.zeros(1, 1, 128)


def test03():
    index_to_word, word_to_index, word_count, corpus_idx = build_vocab()

    _, _, _, corpus_idx = build_vocab()
    lyrics = LyricsDataset(corpus_idx, 5)
    lyrics_dataloader = DataLoader(lyrics, shuffle=False, batch_size=1)
    model = TextGenerator(word_count)

    for x, y in lyrics_dataloader:
        hidden = model.init_hidden()
        print(x.shape)
        model(x, hidden)
        break

4. 构建训练函数

前面的准备工作完成之后, 我们就可以编写训练函数。训练函数主要负责编写数据迭代、送入网络、计算损失、反向传播、更新参数,其流程基本较为固定。

由于我们要实现文本生成,文本生成本质上,输入一串文本,预测下一个文本,也属于分类问题,所以,我们使用多分类交叉熵损失函数。优化方法我们学习过 SGB、AdaGrad、Adam 等,在这里我们选择学习率、梯度自适应的 Adam 算法作为我们的优化方法。

训练完成之后,我们使用 torch.save 方法将模型持久化存储。

def train():

    # 构建词典
    index_to_word, word_to_index, word_count, corpus_idx = build_vocab()
    # 数据集
    lyrics = LyricsDataset(corpus_idx, 32)
    # 初始化模型
    model = TextGenerator(word_count)
    # 损失函数
    criterion = nn.CrossEntropyLoss()
    # 优化方法
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    # 训练轮数
    epoch = 200
    # 迭代打印
    iter_num = 300
    # 训练日志
    train_log = 'lyrics_training.log'
    file = open(train_log, 'w')

    # 开始训练
    for epoch_idx in range(epoch):

        # 数据加载器
        lyrics_dataloader = DataLoader(lyrics, shuffle=True, batch_size=1)
        # 训练时间
        start = time.time()
        # 迭代次数
        iter_num = 0
        # 训练损失
        total_loss = 0.0

        for x, y in lyrics_dataloader:

            # 隐藏状态
            hidden = model.init_hidden()
            # 模型计算
            output, hidden = model(x, hidden)
            # 计算损失
            loss = criterion(output, y.squeeze())
            # 梯度清零
            optimizer.zero_grad()
            # 反向传播
            loss.backward()
            # 参数更新
            optimizer.step()

            iter_num += 1
            total_loss += loss.item()

        message = 'epoch %3s loss: %.5f time %.2f' % \
                  (epoch_idx + 1,
                   total_loss / iter_num,
                   time.time() - start)
        print(message)
        file.write(message + '\n')

    file.close()

    # 模型存储
    torch.save(model.state_dict(), 'model/lyrics_model_%d.bin' % epoch)

5. 构建预测函数

到了最后一步,我们从磁盘加载训练好的模型,进行预测。预测函数,输入第一个指定的词,我们将该词输入网路,预测出下一个词,再将预测的出的词再次送入网络,预测出下一个词,以此类推,知道预测出我们指定长度的内容。

def predict(start_word, sentence_length):

    # 构建词典
    index_to_word, word_to_index, word_count, _ = build_vocab()
    # 构建模型
    model = TextGenerator(vocab_size=word_count)
    # 加载参数
    model.load_state_dict(torch.load('model/lyrics_model_200.bin'))

    # 隐藏状态
    hidden = model.init_hidden()
    # 词转换为索引
    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(index_to_word[idx], end='')
    print()


if __name__ == '__main__':
    predict('分手', 50)

程序运行结果:

分手的话像语言暴力 我已无能为力再提起 决定中断熟悉 周杰伦 周杰伦 一步两步三步四步望著天 看星星 一颗两颗三颗四颗 连成线一步两步三步四步望著天 看星星 一颗两颗三颗四颗 

6. 小节

本小节,带着大家使用学习到的循环神经网络的知识,构建了一个《歌词生成》的项目,该项目的实现流程如下:

  1. 构建词汇表
  2. 构建数据对象
  3. 编写网络模型
  4. 编写训练函数
  5. 编写预测函数