实战5:Python使用循环神经网络生成诗歌

实战5:Python使用循环神经网络生成诗歌

在我们学习了课程8后,我们在实战练习一个例子。

你的主要任务:学习如何使用简单的循环神经网络(Vanilla RNN)生成诗歌。亚历山大·谢尔盖耶维奇·普希金的诗体小说《叶甫盖尼·奥涅金》将作为训练的文本语料库。

使用依赖

python 复制代码
# 不要更改下面的代码块!所有必要的import都列在这里
# __________start of block__________
import string
import os
from random import sample

import numpy as np
import torch, torch.nn as nn
import torch.nn.functional as F

from IPython.display import clear_output

import matplotlib.pyplot as plt
%matplotlib inline
# __________end of block__________
python 复制代码
# 不要更改下面的代码块!
# __________start of block__________
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
print('{} device is available'.format(device))
# __________end of block__________

加载数据

python 复制代码
# 不要更改下面块中的代码
# __________start of block__________
!wget https://raw.githubusercontent.com/MSUcourses/Data-Analysis-with-Python/main/Deep%20Learning/onegin_hw07.txt -O ./onegin.txt

with open('onegin.txt', 'r') as iofile:
    text = iofile.readlines()

text = "".join([x.replace('\t\t', '').lower() for x in text]) # 删除多余的制表符,将所有字母转换为小写
# __________end of block__________

让我们输出输入文本的前几个字符。我们看到制表符已被删除,字母已转换为小写。我们保留 \n 符号,以便教会网络在需要转到新行时生成 \n 符号。

python 复制代码
text[:36]

输出:

词典构建和文本预处理

此任务要求您在符号级别构建语言模型。让我们将所有文本转换为小写,并根据可用文本语料库中的所有字符构建字典。我们还将添加技术令牌<sos>

python 复制代码
# 不要更改下面的代码块!
# __________start of block__________
tokens = sorted(set(text.lower())) + ['<sos>'] # 我们构建一个包含所有符号标记的集合,并将服务标记 <sos> 添加到其中
num_tokens = len(tokens)

assert num_tokens == 84, "Check the tokenization process"

token_to_idx = {x: idx for idx, x in enumerate(tokens)} # 构建一个包含 token 键和 token 列表中索引值的字典
idx_to_token = {idx: x for idx, x in enumerate(tokens)} # 构建一个反向字典(这样你就可以通过索引获取一个标记)

assert len(tokens) == len(token_to_idx), "Mapping should be unique"

print("Seems fine!")


text_encoded = [token_to_idx[x] for x in text]
# __________end of block__________

输出:Seems fine!

其中:

  • set(text.lower()):将原始文本text中的所有字符提取出来,并转换为小写形式,然后使用set函数去除重复字符,得到一个包含文本中所有不同小写字符的集合。

  • sorted(set(text.lower())):对上述集合进行排序,排序后的结果是一个有序的字符列表。这样做的目的是为了确保每次运行代码时,字符的顺序都是固定的,方便后续建立字符到索引的映射。

  • + ['<sos>']:在排好序的字符列表末尾添加一个特殊的服务标记,通常表示 "句子起始(Start of Sentence)" 标记,在文本生成任务中用于指示生成文本的起始位置。

  • token_to_idx构建字符到索引的映射字典:使用字典推导式创建一个名为token_to_idx的字典。enumerate(tokens)会同时返回每个字符在tokens列表中的索引idx和字符本身x。字典中的键是字符x,值是对应的索引idx。这个字典用于将字符转换为模型可以处理的数字索引。

  • idx_to_token构建反向映射字典:同样使用字典推导式创建一个反向映射字典idx_to_token。与token_to_idx相反,这个字典的键是索引idx,值是对应的字符x,用于将模型输出的索引转换回实际的字符。

  • text_encoded使用列表推导式将原始文本text中的每个字符根据token_to_idx字典转换为对应的索引,得到一个编码后的文本列表text_encoded。这个编码后的文本列表将用于后续的模型训练和处理。

你的任务:训练一个经典的循环神经网络(Vanilla RNN)来预测给定文本语料库中的下一个字符,并为固定的初始短语生成长度为 100 的序列。

您可以使用课程7中的代码或参考以下链接:

  • Andrej Karpathy 撰写的关于使用 RNN 的精彩文章:链接
  • Andrej Karpathy 的 Char-rnn 示例:github 存储库
  • 生成莎士比亚诗歌的一个很好的例子:github repo

这个任务相当有创意。如果一开始很难也没关系。在这种情况下,上面列表中的最后一个链接可能特别有用。

下面,为了您的方便,实现了一个函数,该函数从长度为"seq_length"的字符串中生成大小为"batch_size"的随机批次。您可以在训练模型时使用它。(随机生成数据批次,每个批次包含 256 个长度为 100 的字符序列,并且在每个序列的开头添加 标记,为模型训练提供数据:

python 复制代码
# 不要更改下面的代码
# __________start of block__________
batch_size = 256 # 批量大小。批次是一组字符序列。
seq_length = 100 # 一批字符序列的最大长度
start_column = np.zeros((batch_size, 1), dtype=int) + token_to_idx['<sos>'] # 在每行开头添加一个技术符号------确定网络的初始状态

def generate_chunk():
    global text_encoded, start_column, batch_size, seq_length

    start_index = np.random.randint(0, len(text_encoded) - batch_size*seq_length - 1) # 随机选择批次中起始符号的索引
    # 构建一个连续的批次。
    # 为此,我们在源文本中选择一个以索引 start_index 开头且大小为 batch_size*seq_length 的子序列。
    # 然后我们将这个子序列分成大小为 seq_length 的 batch_size 个序列。这将是批次,大小为batch_size*seq_length的矩阵。
    # 矩阵的每一行将包含索引
    data = np.array(text_encoded[start_index:start_index + batch_size*seq_length]).reshape((batch_size, -1))
    yield np.hstack((start_column, data))
# __________end of block__________

其中:

  • batch_size:指的是在一次训练迭代中同时处理的样本数量。在文本生成任务里,每个样本是一个字符序列。这里将 batch_size 设定为 256,意味着每次训练时会处理 256 个字符序列。
  • seq_length:代表每个字符序列的最大长度。在训练时,会把文本分割成长度为 seq_length 的序列,这里设定为 100,即每个序列包含 100 个字符。
  • start_column:这是一个 numpy 数组,形状为 (batch_size, 1)。借助 np.zeros((batch_size, 1), dtype=int) 生成一个全零的数组,再加上 token_to_idx[''],让数组的每个元素都变为 标记对应的索引。此数组用于在每个序列的开头添加 标记,以此确定模型的初始状态。
  • start_index = np.random.randint(0, len(text_encoded) - batch_size*seq_length - 1):随机选取一个索引 start_index,作为当前批次数据在编码后的文本 text_encoded 中的起始位置。这样做的目的是为了让每次生成的批次数据都不同,增加训练数据的随机性。
  • data = np.array(text_encoded[start_index:start_index + batch_size*seq_length]).reshape((batch_size, -1)):从 text_encoded 里选取从 start_index 开始、长度为 batch_size * seq_length 的子序列,将其转换为 numpy 数组,再调整形状为 (batch_size, seq_length) 的矩阵。这个矩阵的每一行就代表一个长度为 seq_length 的字符序列。
  • yield np.hstack((start_column, data)):运用 np.hstack 函数把 start_column 和 data 按水平方向拼接起来,在每个序列的开头添加 标记。yield 关键字让这个函数成为一个生成器,每次调用该函数时,会生成一个新的数据批次,而不是一次性生成所有批次数据,这样可以节省内存。

批次示例:

python 复制代码
next(generate_chunk())

输出:

编写网络:

python 复制代码
# 定义 Vanilla RNN 模型
class VanillaRNN(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(VanillaRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.embedding = nn.Embedding(input_size, hidden_size)
        self.rnn = nn.RNN(hidden_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden):
        x = self.embedding(x)
        out, hidden = self.rnn(x, hidden)
        out = self.fc(out)
        return out, hidden

    def init_hidden(self, batch_size):
        return torch.zeros(self.num_layers, batch_size, self.hidden_size).to(device)

其中:

  • input_size(输入特征的数量,即词汇表的大小)、hidden_size(隐藏层的大小,即隐藏状态的维度)、num_layers(RNN 的层数)和 output_size(输出特征的数量,通常也是词汇表的大小)。

构建模型训练和损失函数曲线图:

python 复制代码
# 超参数设置
input_size = num_tokens
hidden_size = 128
num_layers = 2
output_size = num_tokens
learning_rate = 0.001
num_epochs = 2000  # 这里设置为 10 个 epoch,你可以根据需要调整

# 初始化模型、损失函数和优化器
model = VanillaRNN(input_size, hidden_size, num_layers, output_size).to(device)
criterion = nn.CrossEntropyLoss()
opt = torch.optim.Adam(model.parameters(), lr=learning_rate)

# 用于存储每次迭代损失值的列表
history = []

# 训练循环
for epoch in range(num_epochs):
    for i, batch in enumerate(generate_chunk()):
        inputs = torch.tensor(batch[:, :-1], dtype=torch.long).to(device)
        targets = torch.tensor(batch[:, 1:], dtype=torch.long).to(device)

        hidden = model.init_hidden(batch_size)

        logits, _ = model(inputs, hidden)

        predictions_logp = F.log_softmax(logits, dim=-1)
        loss = criterion(
            predictions_logp.contiguous().view(-1, num_tokens),
            targets.contiguous().view(-1)
        )

        loss.backward()
        opt.step()
        opt.zero_grad()

        history.append(loss.item())
        if (epoch + 1) % 100 == 0:
            clear_output(True)
            plt.plot(history, label='loss')
            plt.legend()
            plt.show()

输出:

其中:

  • inputs = torch.tensor(batch[:, :-1], dtype=torch.long).to(device) 和 targets = torch.tensor(batch[:, 1:], dtype=torch.long).to(device):将批次数据分为输入和目标。batch[:, :-1] 取批次数据的前 seq_length - 1 个字符作为输入,batch[:, 1:] 取批次数据的后 seq_length - 1 个字符作为目标。将它们转换为 torch.Tensor 类型,并移动到指定设备上。

紧接着我们编写文本生成函数,在训练语言模型之后(训练好的神经网络就是语言模型),我们开始进行数据生成。generate_sample 函数通过不断地根据模型的输出进行采样,逐步生成文本,直到达到指定的最大长度:

python 复制代码
# 生成文本函数
def generate_sample(char_rnn, seed_phrase=None, max_length=200, temperature=1.0, device=device):
    '''
    The function generates text given a phrase of length at least SEQ_LENGTH.
    :param seed_phrase: prefix characters. The RNN is asked to continue the phrase
    :param max_length: maximum output length, including seed_phrase
    :param temperature: coefficient for sampling.  higher temperature produces more chaotic outputs,
                        smaller temperature converges to the single most likely output
    '''

    if seed_phrase is not None:
        x_sequence = [token_to_idx['<sos>']] + [token_to_idx[token] for token in seed_phrase]
    else:
        x_sequence = [token_to_idx['<sos>']]

    x_sequence = torch.tensor([x_sequence], dtype=torch.int64).to(device)
    hidden = char_rnn.init_hidden(1)

    # 输入种子短语
    for i in range(len(x_sequence[0]) - 1):
        _, hidden = char_rnn(x_sequence[:, i].unsqueeze(1), hidden)

    # 生成剩余文本
    for _ in range(max_length - len(x_sequence[0])):
        logits, hidden = char_rnn(x_sequence[:, -1].unsqueeze(1), hidden)
        probs = F.softmax(logits / temperature, dim=-1)
        next_token = torch.multinomial(probs.view(-1), num_samples=1)
        x_sequence = torch.cat([x_sequence, next_token.unsqueeze(0)], dim=1)

    return ''.join([tokens[ix] for ix in x_sequence.cpu().data.numpy()[0]])
  • 参数:
    • char_rnn:已经训练好的 RNN 模型,用于生成文本。
    • seed_phrase:种子短语,是一个字符串,用于作为生成文本的起始部分。如果为 None,则从 标记开始生成。
    • max_length:生成文本的最大长度,包含种子短语的长度,默认值为 200。
    • temperature:采样系数,用于控制生成文本的随机性。较高的温度会产生更随机、混乱的输出;较低的温度会使输出更倾向于选择概率最大的字符,默认值为 1.0。
    • device:指定运行模型的设备(如 CPU 或 GPU),默认使用之前定义的 device。
  • 生成剩余文本:
    通过循环不断生成新的字符,直到达到 max_length,在每次循环中:
    • 将当前序列的最后一个字符输入到 RNN 模型中,得到模型的输出 logits 和更新后的隐藏状态 hidden。
    • logits / temperature:通过温度参数 temperature 调整 logits 的值,较高的温度会使概率分布更加均匀,增加随机性;较低的温度会使概率分布更加集中,输出更倾向于概率最大的字符。
    • F.softmax(logits / temperature, dim=-1):对调整后的 logits 应用 softmax 函数,将其转换为概率分布 probs。
    • torch.multinomial(probs.view(-1), num_samples=1):根据概率分布 probs 进行采样,随机选择一个字符的索引作为下一个字符。
    • torch.cat([x_sequence, next_token.unsqueeze(0)], dim=1):将采样得到的下一个字符的索引添加到 x_sequence 中。

下面是经过训练的模型生成的文本示例。文本中包含大量不存在的单词并不可怕。所使用的模型非常简单:它是一个简单的经典 RNN。

python 复制代码
print(generate_sample(model, ' мой дядя самых честных правил', max_length=500, temperature=0.8))

输出:

总代码

python 复制代码
# 不要更改下面的代码块!所有必要的import都列在这里
# __________start of block__________
import string
import os
from random import sample

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

from IPython.display import clear_output

import matplotlib.pyplot as plt
%matplotlib inline
# __________end of block__________
# 不要更改下面的代码块!
# __________start of block__________
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
print('{} device is available'.format(device))
# __________end of block__________
# 不要更改下面块中的代码
# __________start of block__________
!wget https://raw.githubusercontent.com/MSUcourses/Data-Analysis-with-Python/main/Deep%20Learning/onegin_hw07.txt -O./onegin.txt

with open('onegin.txt', 'r') as iofile:
    text = iofile.readlines()

text = "".join([x.replace('\t\t', '').lower() for x in text])  # Убираем лишние символы табуляций, приводим все буквы к нижнему регистру
# __________end of block__________
# 不要更改下面的代码块!
# __________start of block__________
tokens = sorted(set(text.lower())) + ['<sos>']  # 我们构建一个包含所有符号标记的集合,并将服务标记 <sos> 添加到其中
num_tokens = len(tokens)

assert num_tokens == 84, "Check the tokenization process"

token_to_idx = {x: idx for idx, x in enumerate(tokens)}  # 构建一个包含 token 键和 token 列表中索引值的字典
idx_to_token = {idx: x for idx, x in enumerate(tokens)}  # 构建一个反向字典(这样你就可以通过索引获取一个标记)

assert len(tokens) == len(token_to_idx), "Mapping should be unique"

print("Seems fine!")


text_encoded = [token_to_idx[x] for x in text]
# __________end of block__________
# 不要更改下面的代码
# __________start of block__________
batch_size = 256  # 批量大小。批次是一组字符序列。
seq_length = 100  # 一批字符序列的最大长度
start_column = np.zeros((batch_size, 1), dtype=int) + token_to_idx['<sos>']  # 在每行开头添加一个技术符号------确定网络的初始状态

def generate_chunk():
    global text_encoded, start_column, batch_size, seq_length

    start_index = np.random.randint(0, len(text_encoded) - batch_size * seq_length - 1)  # 随机选择批次中起始符号的索引
    # 构建一个连续的批次。
    # 为此,我们在源文本中选择一个以索引 start_index 开头且大小为 batch_size*seq_length 的子序列。
    # 然后我们将这个子序列分成大小为 seq_length 的 batch_size 个序列。这将是批次,大小为batch_size*seq_length的矩阵。
    # 矩阵的每一行将包含索引
    data = np.array(text_encoded[start_index:start_index + batch_size * seq_length]).reshape((batch_size, -1))
    yield np.hstack((start_column, data))
# __________end of block__________


# 定义 Vanilla RNN 模型
class VanillaRNN(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(VanillaRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.embedding = nn.Embedding(input_size, hidden_size)
        self.rnn = nn.RNN(hidden_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x, hidden):
        x = self.embedding(x)
        out, hidden = self.rnn(x, hidden)
        out = self.fc(out)
        return out, hidden

    def init_hidden(self, batch_size):
        return torch.zeros(self.num_layers, batch_size, self.hidden_size).to(device)


# 超参数设置
input_size = num_tokens
hidden_size = 128
num_layers = 2
output_size = num_tokens
learning_rate = 0.001
num_epochs = 2000  # 这里设置为 10 个 epoch,你可以根据需要调整

# 初始化模型、损失函数和优化器
model = VanillaRNN(input_size, hidden_size, num_layers, output_size).to(device)
criterion = nn.CrossEntropyLoss()
opt = torch.optim.Adam(model.parameters(), lr=learning_rate)

# 用于存储每次迭代损失值的列表
history = []

# 训练循环
for epoch in range(num_epochs):
    for i, batch in enumerate(generate_chunk()):
        inputs = torch.tensor(batch[:, :-1], dtype=torch.long).to(device)
        targets = torch.tensor(batch[:, 1:], dtype=torch.long).to(device)

        hidden = model.init_hidden(batch_size)

        logits, _ = model(inputs, hidden)

        predictions_logp = F.log_softmax(logits, dim=-1)
        loss = criterion(
            predictions_logp.contiguous().view(-1, num_tokens),
            targets.contiguous().view(-1)
        )

        loss.backward()
        opt.step()
        opt.zero_grad()

        history.append(loss.item())
        if (epoch + 1) % 100 == 0:
            clear_output(True)
            plt.plot(history, label='loss')
            plt.legend()
            plt.show()

# 生成文本函数
def generate_sample(char_rnn, seed_phrase=None, max_length=200, temperature=1.0, device=device):
    if seed_phrase is not None:
        x_sequence = [token_to_idx['<sos>']] + [token_to_idx[token] for token in seed_phrase]
    else:
        x_sequence = [token_to_idx['<sos>']]

    x_sequence = torch.tensor([x_sequence], dtype=torch.int64).to(device)
    hidden = char_rnn.init_hidden(1)

    # 输入种子短语
    for i in range(len(x_sequence[0]) - 1):
        _, hidden = char_rnn(x_sequence[:, i].unsqueeze(1), hidden)

    # 生成剩余文本
    while len(x_sequence[0]) < max_length:
        logits, hidden = char_rnn(x_sequence[:, -1].unsqueeze(1), hidden)
        probs = F.softmax(logits / temperature, dim=-1)
        next_token = torch.multinomial(probs.view(-1), num_samples=1)
        x_sequence = torch.cat([x_sequence, next_token.unsqueeze(0)], dim=1)
        # print(f"当前生成的序列长度: {len(x_sequence[0])}")

    return ''.join([tokens[ix] for ix in x_sequence.cpu().data.numpy()[0]])

# 添加你提供的测试文本生成
print(generate_sample(model, 'мой дядя самых честных правил', max_length=500, temperature=0.8))
相关推荐
一只小小汤圆4 分钟前
如何xml序列化 和反序列化类中包含的类
xml·开发语言·c#
硅谷秋水5 分钟前
TASTE-Rob:推进面向任务的手-目标交互视频生成,实现可通用的机器人操作
人工智能·深度学习·机器学习·计算机视觉·机器人·交互
yzx9910138 分钟前
柑橘检测模型
服务器·人工智能·深度学习·算法
南枝异客11 分钟前
电话号码的字母组合
开发语言·javascript·算法
啊哈哈哈哈哈啊哈哈33 分钟前
G1周打卡——GAN入门
pytorch·深度学习·生成对抗网络
未来并未来35 分钟前
Sentinel 流量控制安装与使用
开发语言·python·sentinel
神齐的小马37 分钟前
机器学习 [白板推导](六)[核方法、指数族分布]
人工智能·机器学习
孚为智能科技44 分钟前
集装箱残损识别系统如何检测残损?它的识别率能达到多少?
大数据·图像处理·人工智能·计算机视觉·视觉检测
Halo_tjn1 小时前
Java IO
java·开发语言
小白学大数据1 小时前
爬取汽车之家评论并利用NLP进行关键词提取
人工智能·自然语言处理·汽车