【循环神经网络-03】:文本生成案例

文本生成案例

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

数据集如下:

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

数据集共有 5819 行。

1 构词词典

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

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

  • word_to_index: 存储了词到编号的映射
  • index_to_word: 存储了编号到词的映射
    一般构建词表的流程如下:
  1. 语料清洗, 去除不相关的内容
  2. 对语料进行分词
  3. 构建词表

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

import torch
import re
import jieba
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import time


# 构建词典
def build_vocab():
    fname = 'data/jaychou_lyrics.txt'

    # 1. 文本数据清洗
    clean_sentences = []
    for line in open(fname, 'r',encoding="utf-8"):

        # 去除指定的一些内容
        line = line.replace('韩语Rap译文〗', '')
        # 保留中文、数字、部分的标点符号
        line = re.sub(r'[^\u4e00-\u9fa5 a-zA-Z0-9!?,]', '', line)
        # 连续空格替换
        line = re.sub(r'[ ]{2,}', '', line)
        # 去除行两侧的空白和换行符
        line = line.strip()
        # 去除单字的行
        if len(line) <= 1:
            continue

        if line not in clean_sentences:
            clean_sentences.append(line)

    # print(clean_sentences)

    # 2. 分词
    all_sentences = []
    index_to_word = []   # 重要的词表: 索引到词的映射

    # 1.遍历所有的句子
    for line in clean_sentences:
        # 对每个句子进行分词,得到列表
        words = jieba.lcut(line)
        # 将每个句子对应的列表添加到all_sentences中
        # 便于后面我们将句子转换为索引表示
        all_sentences.append(words)

        # 遍历分词列表中的所有词
        for word in words:
            # 添加到index_to_word表中
            # index_to_word - 是包含所有词汇的表
            if word not in index_to_word:
                index_to_word.append(word)

    # print(all_sentences)
    # print(index_to_word)

    # 重要词表: 词到索引的映射
    # 使用enumerate方便构建每个词到index的索引,这里的index_to_word表没有进行去重
    word_to_index = {word: idx for idx, word in enumerate(index_to_word)}

    # 3. 将输入的语料转换为索引表示
    corpus_index = []   # 语料的索引表示
    # 3.1 遍历每个句子对应的的列表
    for sentence in all_sentences:
        temp = []
        # 3.2 遍历每个句子的词,找到对应的索引
        # 将词对应的索引放到一个temp表中
        for word in sentence:
            temp.append(word_to_index[word])

        # 在每行歌词的后面添加一个空格
        # 这里添加的是空格的索引值
        temp.append(word_to_index[' '])
        corpus_index.extend(temp)

    # print(corpus_index)
    return index_to_word, word_to_index, len(index_to_word), corpus_index

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 对象进行迭代可以获取语料,并将其送入网络。

# 编写数据集类
# 本质是自定义的可迭代对象
# dataloader会遍历这个可迭代对象,内部生成一个生成器
# 从而取出所有数据
class LyricsDataset:
    # 传入语料索引列表
    # num_chars - 语料的长度

    # 1. 实现__init__
    def __init__(self, corpus_index, num_chars):
        """
        :param corpus_index: 语料的索引表示
        """

        # 语料数据
        self.corpus_index = corpus_index
        # 语料长度
        self.num_chars = num_chars
        # 语料词的总数量
        self.word_count = len(corpus_index)
        # 计算句子长度有多少个
        self.number = self.word_count //  self.num_chars

    # 实现__len__ 返回数据集的长度
    def __len__(self):
        return self.number

    # __getitem__ 根据index获取到对应的数据
    def __getitem__(self, idx):
        # 输入的 idx 可能不是合法的
        start = min(max(idx, 0), self.word_count - self.num_chars - 2)
        # 获取一条样本,就会有 x, 就会有 y
        x = self.corpus_index[start: start + self.num_chars]
        y = self.corpus_index[start + 1: start + 1 + self.num_chars]

        # x = 0, 1, 2, 39, 0
        # y = 1, 2, 39, 0, 3
        
        return torch.tensor(x), torch.tensor(y)


def test02():

    # 获取词向量表,word长度,语料索引列表
    index_to_word, word_to_index, word_len, corpus_index = build_vocab()

    # 自定义数据集对象
    # 相当于是一个可迭代对象
    lyrics = LyricsDataset(corpus_index, 1)
    # 注意: batch_size = 1

    # 每次自动从lyrics中取出batch_size个样本
    dataloader = DataLoader(lyrics, shuffle=False, batch_size=5)

    for x, y in dataloader:
        print(x)
        print(y)
        break

3 构建网络模型

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

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

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

示例代码如下:

# 构建循环神经网络
class TextGenerator(nn.Module):

    def __init__(self):
        
        super(TextGenerator, self).__init__()

        # 初始化词嵌入层
        # 词嵌入层,传入的是全部词汇的数量
        # 维度采用128个维度表示一个词
        self.ebd = nn.Embedding(num_embeddings=word_len, embedding_dim=128)
        # 初始化循环网络层
        self.rnn = nn.RNN(input_size=128, hidden_size=128)
        # 初始化输出层, 预测的标签数量为词典中词的总数量
        self.out = nn.Linear(128, word_len)

    # inputs(1,5)
    def forward(self, inputs, hidden):

        embed = self.ebd(inputs)
        # embed 的形状是 (1, 5, 128)
        # print(embed.shape)

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

        # 送入循环网络层
        # output 表示的是每一个时间步的输出
        # 输入(seq_len,batch_size,hidden_size)
        # 词嵌入层输出结果是(batch_size,seq_len,hidden_size),所以对词嵌入层的输出结果进行交换维度
        output, hidden = self.rnn(embed.transpose(0, 1), hidden)

        # 将 output 送入到全连接层得到输出
        output = self.out(output)

        return output, hidden

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


def test02():

    index_to_word, word_to_index, word_len, corpus_index = build_vocab()
    lyrics = LyricsDataset(corpus_index, 5)
    # 注意: batch_size = 1
    dataloader = DataLoader(lyrics, shuffle=False, batch_size=1)

    # 初始化网络对象
    model = TextGenerator()

    for x, y in dataloader:
        # 初始化隐藏状态
        hidden = model.init_hidden()
        # x shape (1,5) 词向量长度为5,每次送入一个样本
        y_pred, hidden = model(x, hidden)
        print(y_pred.shape)
        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 train():

    # 构建词典
    index_to_word, word_to_index, word_len, corpus_index = build_vocab()
    # 数据集
    lyrics = LyricsDataset(corpus_index, 32)
    # 初始化模型
    model = TextGenerator()
    # 损失函数
    criterion = nn.CrossEntropyLoss()
    # 优化方法
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    # 训练轮数
    epoch = 10
    # 迭代打印
    iter_num = 300

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

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

        for x, y in dataloader:

            # 初始化隐藏状态
            hidden = model.init_hidden()
            # 送入网络计算
            output, _ = model(x, hidden)
            # 计算损失
            print(output.shape) # torch.Size([32, 1, 5682])
            print(y.shape) # torch.Size([1, 32])
            loss = criterion(output.squeeze(), y.squeeze())
            # 梯度清零
            optimizer.zero_grad()
            # 反向传播
            loss.backward()
            # 参数更新
            optimizer.step()

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


        info = 'epoch:%3s loss:%.5f time:%.2f' % \
               (epoch_idx,
                total_loss / iter_num,
                time.time() - start)

        print(info)

    # 模型保存
    torch.save(model.state_dict(), 'model/text-generator.pth')


# 预测函数
def predict(start_word, sentence_length):

    # 构建词典
    index_to_word, word_to_index, word_len, corpus_index = build_vocab()

    # 加载模型
    model = TextGenerator()
    model.load_state_dict(torch.load('model/text-generator.pth'))
    model.eval()

    # 初始化隐藏状态
    hidden = model.init_hidden()

    # 首先, 将 start_word 转换为索引
    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 完整代码

import torch
import re
import jieba
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import time


# 构建词典
def build_vocab():
    fname = 'data/jaychou_lyrics.txt'

    # 1. 文本数据清洗
    clean_sentences = []
    for line in open(fname, 'r',encoding="utf-8"):

        # 去除指定的一些内容
        line = line.replace('韩语Rap译文〗', '')
        # 保留中文、数字、部分的标点符号
        line = re.sub(r'[^\u4e00-\u9fa5 a-zA-Z0-9!?,]', '', line)
        # 连续空格替换
        line = re.sub(r'[ ]{2,}', '', line)
        # 去除行两侧的空白和换行符
        line = line.strip()
        # 去除单字的行
        if len(line) <= 1:
            continue

        if line not in clean_sentences:
            clean_sentences.append(line)

    # print(clean_sentences)

    # 2. 分词
    all_sentences = []
    index_to_word = []   # 重要的词表: 索引到词的映射

    # 1.遍历所有的句子
    for line in clean_sentences:
        # 对每个句子进行分词,得到列表
        words = jieba.lcut(line)
        # 将每个句子对应的列表添加到all_sentences中
        # 便于后面我们将句子转换为索引表示
        all_sentences.append(words)

        # 遍历分词列表中的所有词
        for word in words:
            # 添加到index_to_word表中
            # index_to_word - 是包含所有词汇的表
            if word not in index_to_word:
                index_to_word.append(word)

    # print(all_sentences)
    # print(index_to_word)

    # 重要词表: 词到索引的映射
    # 使用enumerate方便构建每个词到index的索引,这里的index_to_word表没有进行去重
    word_to_index = {word: idx for idx, word in enumerate(index_to_word)}

    # 3. 将输入的语料转换为索引表示
    corpus_index = []   # 语料的索引表示
    # 3.1 遍历每个句子对应的的列表
    for sentence in all_sentences:
        temp = []
        # 3.2 遍历每个句子的词,找到对应的索引
        # 将词对应的索引放到一个temp表中
        for word in sentence:
            temp.append(word_to_index[word])

        # 在每行歌词的后面添加一个空格
        # 这里添加的是空格的索引值
        temp.append(word_to_index[' '])
        corpus_index.extend(temp)

    # print(corpus_index)
    return index_to_word, word_to_index, len(index_to_word), corpus_index


# 调用构建词典的函数
index_to_word, word_to_index, word_len, corpus_index = build_vocab()


# 编写数据集类
# 本质是自定义的可迭代对象
# dataloader会遍历这个可迭代对象,内部生成一个生成器
# 从而取出所有数据
class LyricsDataset:
    # 传入语料索引列表
    # num_chars - 语料的长度

    # 1. 实现__init__
    def __init__(self, corpus_index, num_chars):
        """
        :param corpus_index: 语料的索引表示
        """

        # 语料数据
        self.corpus_index = corpus_index
        # 语料长度
        self.num_chars = num_chars
        # 语料词的总数量
        self.word_count = len(corpus_index)
        # 计算句子长度有多少个
        self.number = self.word_count //  self.num_chars

    # 实现__len__ 返回数据集的长度
    def __len__(self):
        return self.number

    # __getitem__ 根据index获取到对应的数据
    def __getitem__(self, idx):
        # 输入的 idx 可能不是合法的
        start = min(max(idx, 0), self.word_count - self.num_chars - 2)
        # 获取一条样本,就会有 x, 就会有 y
        x = self.corpus_index[start: start + self.num_chars]
        y = self.corpus_index[start + 1: start + 1 + self.num_chars]

        # x = 0, 1, 2, 39, 0
        # y = 1, 2, 39, 0, 3
        
        return torch.tensor(x), torch.tensor(y)


def test01():

    # 获取词向量表,word长度,语料索引列表
    index_to_word, word_to_index, word_len, corpus_index = build_vocab()

    # 自定义数据集对象
    # 相当于是一个可迭代对象
    lyrics = LyricsDataset(corpus_index, 5)
    # 注意: batch_size = 1

    # 每次自动从lyrics中取出batch_size个样本
    dataloader = DataLoader(lyrics, shuffle=False, batch_size=5)

    for x, y in dataloader:
        print(x)
        print(y)
        break


# 构建循环神经网络
class TextGenerator(nn.Module):

    def __init__(self):
        
        super(TextGenerator, self).__init__()

        # 初始化词嵌入层
        # 词嵌入层,传入的是全部词汇的数量
        # 维度采用128个维度表示一个词
        self.ebd = nn.Embedding(num_embeddings=word_len, embedding_dim=128)
        # 初始化循环网络层
        self.rnn = nn.RNN(input_size=128, hidden_size=128)
        # 初始化输出层, 预测的标签数量为词典中词的总数量
        self.out = nn.Linear(128, word_len)

    # inputs(1,5)
    def forward(self, inputs, hidden):

        embed = self.ebd(inputs)
        # embed 的形状是 (1, 5, 128)
        # print(embed.shape)

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

        # 送入循环网络层
        # output 表示的是每一个时间步的输出
        # 输入(seq_len,batch_size,hidden_size)
        # 词嵌入层输出结果是(batch_size,seq_len,hidden_size),所以对词嵌入层的输出结果进行交换维度
        output, hidden = self.rnn(embed.transpose(0, 1), hidden)

        print(output.shape)
        # 将 output 送入到全连接层得到输出
        output = self.out(output)

        return output, hidden

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


def test02():

    index_to_word, word_to_index, word_len, corpus_index = build_vocab()
    lyrics = LyricsDataset(corpus_index, 5)
    # 注意: batch_size = 1
    dataloader = DataLoader(lyrics, shuffle=False, batch_size=1)

    # 初始化网络对象
    model = TextGenerator()

    for x, y in dataloader:
        # 初始化隐藏状态
        hidden = model.init_hidden()
        # x shape (1,5) 词向量长度为5,每次送入一个样本
        y_pred, hidden = model(x, hidden)
        print(y_pred.shape)
        break


# 训练函数
def train():

    # 构建词典
    index_to_word, word_to_index, word_len, corpus_index = build_vocab()
    # 数据集
    lyrics = LyricsDataset(corpus_index, 32)
    # 初始化模型
    model = TextGenerator()
    # 损失函数
    criterion = nn.CrossEntropyLoss()
    # 优化方法
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    # 训练轮数
    epoch = 10
    # 迭代打印
    iter_num = 300

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

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

        for x, y in dataloader:

            # 初始化隐藏状态
            hidden = model.init_hidden()
            # 送入网络计算
            output, _ = model(x, hidden)
            # 计算损失
            print(output.shape) # torch.Size([32, 1, 5682])
            print(y.shape) # torch.Size([1, 32])
            loss = criterion(output.squeeze(), y.squeeze())
            # 梯度清零
            optimizer.zero_grad()
            # 反向传播
            loss.backward()
            # 参数更新
            optimizer.step()

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


        info = 'epoch:%3s loss:%.5f time:%.2f' % \
               (epoch_idx,
                total_loss / iter_num,
                time.time() - start)

        print(info)

    # 模型保存
    torch.save(model.state_dict(), 'model/text-generator.pth')


# 预测函数
def predict(start_word, sentence_length):

    # 构建词典
    index_to_word, word_to_index, word_len, corpus_index = build_vocab()

    # 加载模型
    model = TextGenerator()
    model.load_state_dict(torch.load('model/text-generator.pth'))
    model.eval()

    # 初始化隐藏状态
    hidden = model.init_hidden()

    # 首先, 将 start_word 转换为索引
    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__':
    # test01()
    # test02()
    # build_vocab()
    # train()
    # predict('分手', 50)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值