全网最详细使用PyTorch实现循环神经网络

Posted 轩Scott

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全网最详细使用PyTorch实现循环神经网络相关的知识,希望对你有一定的参考价值。

目录

1. 什么是循环神经网络

2. PyTorch中的循环神经网络

3. 创建循环神经网络模型

小结

4.训练循环神经网络模型

5.评估循环神经网络模型


欢迎来到这篇使用PyTorch实现循环神经网络的教程!在这里,我将向您展示如何使用PyTorch创建、训练和评估一个循环神经网络(RNN),并将其应用于文本生成任务。这篇教程将涵盖以下主题:

1.什么是循环神经网络

2.PyTorch中的循环神经网络

3.创建循环神经网络模型

4.训练循环神经网络模型

5.评估循环神经网络模型

6.应用循环神经网络模型于文本生成

让我们开始吧!

1. 什么是循环神经网络

循环神经网络(RNN)是一种用于序列数据的神经网络,常用于语言建模、翻译和音乐生成等任务。RNN是基于前一个时间步的输入和当前时间步的状态来预测下一个时间步的输出。这使得RNN在处理连续数据时非常有效,因为它可以利用上一个时间步的信息来更新其状态并生成新的输出。

RNN模型的核心是“循环”结构。在传统的神经网络中,每个输入都是独立地处理的,而在RNN中,每个输入都与前一个时间步的输出相关联。在每个时间步,RNN将当前输入和前一个时间步的输出传递到一个称为“隐藏状态”的向量中。该隐藏状态捕获了当前输入和过去输入的信息,并将其用于预测下一个输出。

2. PyTorch中的循环神经网络

PyTorch是一个非常流行的深度学习框架,提供了许多构建和训练神经网络的工具。在PyTorch中,我们可以使用torch.nn模块中的RNN类和LSTM类来构建RNN模型。

RNN类是最基本的RNN类型。它接收一个输入张量和一个初始隐藏状态张量,并输出一个输出张量和一个最终隐藏状态张量。LSTM类是一种改进的RNN类型,它使用一种称为长短期记忆(Long Short-Term Memory,LSTM)的结构来记忆和管理状态。LSTM比RNN更适合处理长序列数据,因为它可以更好地处理梯度消失和梯度爆炸的问题。

在接下来的部分中,我们将使用PyTorch中的RNN类来构建一个简单的循环神经网络模型,并将其用于文本生成任务。

3. 创建循环神经网络模型

在这个示例中,我们将创建一个字符级别的文本生成模型,它将一个或多个字符作为输入,并生成下一个字符作为输出。我们将使用莎士比亚的一些作品作为我们的训练数据,以生成新的莎士比亚风格的文本。

首先,我们需要对文本进行预处理。我们将把所有的字符转换成小写,并将其表示为数字。我们还将创建一个字典,将每个字符映射到一个唯一的数字,并将这些数字用于训练模型。

import string 

def preprocess(text): 
    # 将文本转换为小写 
    text = text.lower() 

    # 删除所有标点符号 
    text = text.translate(str.maketrans('', '', string.punctuation)) 

    # 创建一个字符到数字的映射表 
    char_to_index = char: i for i, char in enumerate(set(text)) 
    index_to_char = i: char for char, i in char_to_index.items() 

    # 将文本表示为数字 
    text = [char_to_index[char] for char in text] 

    return text, char_to_index, index_to_char

接下来,我们将创建一个TextDataset类,用于加载和处理训练数据。该类将接收一个预处理过的文本列表,并将其转换为PyTorch张量。

import torch 
from torch.utils.data import Dataset 
class TextDataset(Dataset): 
    def __init__(self, text): 
        self.text = torch.tensor(text) 

    def __len__(self): 
        return len(self.text) - 1 

    def __getitem__(self, idx): 
        x = self.text[idx] 
        y = self.text[idx+1] 
        return x, y

接下来,我们将创建一个RNN类来实现我们的循环神经网络模型。该模型将包含一个嵌入层、一个RNN层和一个全连接层。嵌入层将输入字符的数字表示转换为向量表示,RNN层将处理序列数据并生成隐藏状态,全连接层将根据隐藏状态生成输出。

import torch.nn as nn 
class RNN(nn.Module): 
    def __init__(self, input_size, hidden_size, output_size): 
        super(RNN, self).__init__()
 
        self.embedding = nn.Embedding(input_size, hidden_size) 
        self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True) 
        self.fc = nn.Linear(hidden_size, output_size) 
 
    def forward(self, x, h): 
        x = self.embedding(x) 
        out, h = self.rnn(x, h) 
        out = self.fc(out) 
        return out, h

我们还需要定义一些超参数,例如序列长度、批大小、隐藏层大小和训练迭代次数。

# 定义超参数 
sequence_length = 100 
batch_size = 128 
hidden_size = 256 
num_layers = 2 
num_epochs = 50 
learning_rate = 0.001

我们还需要定义一个函数来生成输入和目标序列。该函数将根据指定的序列长度将文本数据分割成多个序列,并将每个序列转换为PyTorch张量。

def generate_sequence_data(text, sequence_length, batch_size): 
    num_sequences = len(text) // sequence_length 
    text = text[:num_sequences * sequence_length] 
    x_data = text[:-1] 
    y_data = text[1:] 

    x_batches = torch.split(x_data, batch_size) 
    y_batches = torch.split(y_data, batch_size) 

    sequence_data = [] 
    for i in range(num_sequences // batch_size): 
        x = x_batches[i * batch_size:(i + 1) * batch_size] 
        y = y_batches[i * batch_size:(i + 1) * batch_size] 
        sequence_data.append((torch.stack(x), torch.stack(y))) 

    return sequence_data

接下来,我们将加载并预处理我们的训练数据,并将其转换为PyTorch数据集。

with open('shakespeare.txt', 'r', encoding='utf-8') as f: 
    text = f.read() 

text, char_to_index, index_to_char = preprocess(text) 
dataset = TextDataset(text)

现在,我们可以创建一个DataLoader对象,用于加载数据并将其分成批次。

dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)

接下来,我们将创建一个RNN对象,并定义我们的损失函数和优化器。

model = RNN(len(char_to_index), hidden_size, len(char_to_index)).to(device) 
criterion = nn.CrossEntropyLoss() 
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

现在我们已经准备好训练我们的模型了。在每个训练迭代中,我们将向模型提供一个输入序列并计算模型的输出。我们将使用损失函数来计算模型输出和目标序列之间的差异,并使用反向传播算法更新模型参数。

for epoch in range(num_epochs): 
    total_loss = 0 
    h = torch.zeros(num_layers, batch_size, hidden_size).to(device) 
for i, (x, y) in enumerate(dataloader): 
        x = x.to(device) 
        y = y.to(device) 

        optimizer.zero_grad() 
        out, h = model(x, h) 
        loss = criterion(out.view(-1, len(char_to_index)), y.view(-1)) 
        loss.backward() 
        optimizer.step() 

        total_loss += loss.item() 
        if (i+1) % 100 == 0: 
            print('Epoch [/], Step [/], Loss: :.4f' 
                .format(epoch+1, num_epochs, i+1, len(dataset)//batch_size, total_loss / (i+1)))

在训练完成后,我们可以使用我们的训练好的模型来生成文本。我们可以通过提供一个起始字符并循环多次生成文本。在每次生成文本时,我们将向模型提供当前字符并获取下一个字符的预测

def generate_text(model, char_to_index, index_to_char, sequence_length, seed_text, num_chars): 
    with torch.no_grad(): 
        h = torch.zeros(num_layers, 1, hidden_size).to(device) 
        seed = [char_to_index[char] for char in seed_text] 
        input = torch.tensor(seed).unsqueeze(1).to(device) 
        output = [] 
        for i in range(num_chars): 
            out, h = model(input, h) 
            out = out[-1] 
            prob = nn.functional.softmax(out, dim=0).cpu().numpy() 
            index = np.random.choice(len(char_to_index), p=prob) 
            output.append(index_to_char[index]) 
            input = torch.tensor([index]).unsqueeze(1).to(device) 
        return ''.join(output)

现在,我们可以使用以下代码段训练我们的模型并生成文本。

# 定义超参数 
sequence_length = 100 
batch_size = 128 
hidden_size = 256 
num_layers = 2 
num_epochs = 50 
learning_rate = 0.001 

# 加载数据集 
with open('shakespeare.txt', 'r', encoding='utf-8') as f: 
    text = f.read() 

text, char_to_index, index_to_char = preprocess(text) 
dataset = TextDataset(text) 

# 创建数据加载器 
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) 

# 创建模型,损失函数和优化器 
model = RNN(len(char_to_index), hidden_size, len(char_to_index)).to(device) 
criterion = nn.CrossEntropyLoss() 
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) 

# 训练模型 
for epoch in range(num_epochs): 
    total_loss = 0 
    h = torch.zeros(num_layers, batch_size, hidden_size).to(device) 
    for i, (x, y) in enumerate(dataloader):
        x = x.to(device) 
        y = y.to(device) 

        optimizer.zero_grad() 
        out, h = model(x, h) 
        loss = criterion(out.view(-1, len(char_to_index)), y.view(-1)) 
        loss.backward() 
        optimizer.step() 

        total_loss += loss.item() 
        
        if (i+1) % 100 == 0: 
            print('Epoch [/], Step [/], Loss: :.4f' 
                .format(epoch+1, num_epochs, i+1, len(dataset)//batch_size, total_loss / (i+1))) 

# 生成文本 
generated_text = generate_text(model, char_to_index, index_to_char, sequence_length, 'shall i compare thee to a summer\\'s day?\\n', 1000) 
print(generated_text)

这将训练我们的循环神经网络,并使用它生成一段长度为1000个字符的文本。您可以尝试不同的超参数和训练数据来创建不同的模型和生成文本。

小结

在本教程中,我们介绍了循环神经网络(RNN)及其应用,使用 PyTorch 实现了一个基本的字符级 RNN 模型,以生成文本为例子。我们首先预处理文本数据集,然后定义了一个 RNN 模型。我们使用 Cross Entropy 损失函数和 Adam 优化器来训练我们的模型,并通过生成一些示例文本来验证它是否有效。

总的来说,RNN 在处理序列数据时非常有用,如自然语言处理、时间序列预测等。PyTorch 为开发人员提供了非常方便的工具和接口,使其能够轻松地构建和训练深度学习模型。同时,使用 GPU 可以加速模型的训练和推理过程。因此,我们希望本教程能够为您提供有关 PyTorch 和 RNN 的基础知识,并启发您开始尝试更复杂的深度学习任务。

接下来,我们将介绍如何训练循环神经网络模型、评估循环神经网络模型,并将循环神经网络模型应用于文本生成。

4.训练循环神经网络模型

我们的模型已经定义好了,接下来就是要训练模型。我们需要为模型提供训练数据,然后通过计算损失和反向传播更新模型参数。在此之前,我们需要设置一些训练的超参数,例如学习率、迭代次数等等。

learning_rate = 0.005 
n_epochs = 2000 
print_every = 100 
plot_every = 10 
hidden_size = 100
 
# Create RNN model and set loss and optimizer 
rnn = RNN(n_characters, hidden_size, n_characters) 
loss_fn = nn.CrossEntropyLoss() 
optimizer = torch.optim.Adam(rnn.parameters(), lr=learning_rate) 

# Initialize loss and time variables 
current_loss = 0 
all_losses = [] 
start_time = time.time()

然后我们开始训练循环神经网络模型:

for epoch in range(1, n_epochs + 1): 
    # Get input and target tensors from random batch 
    input_tensor, target_tensor = random_training_example(all_characters, n_characters,         
    data, chunk_len) # Initialize hidden state hidden = rnn.init_hidden() 

    # Zero the gradients 
    optimizer.zero_grad() 

    # Forward pass 
    loss, hidden = rnn(input_tensor, target_tensor, hidden) 

    # Backward pass 
    loss.backward() 

    # Update parameters 
    optimizer.step() 

    # Update loss 
    current_loss += loss.item() 

    # Print progress 
    if epoch % print_every == 0: 
        print(f'Epoch [epoch/n_epochs], Loss: current_loss/print_every:.4f') 
        current_loss = 0

    # Save the model and the optimizer every 500 epochs 
    if epoch % 500 == 0: 
        torch.save(rnn.state_dict(), f'./models/rnn_epoch.pth')                         
        torch.save(optimizer.state_dict(), f'./models/optimizer_epoch.pth') 

    # Record the average loss every `plot_every` epochs 
    if epoch % plot_every == 0: 
        all_losses.append(current_loss / plot_every) 
        current_loss = 0

在训练过程中,我们首先从数据集中随机获取一批训练样本。然后,我们初始化隐藏状态,将梯度归零,通过前向传播计算损失,然后通过反向传播更新模型参数。最后,我们记录每个时期的损失,并在训练完成后将损失可视化。在训练过程中,我们还可以保存模型和优化器的状态,以便稍后恢复模型。

5.评估循环神经网络模型

一旦我们训练好了模型,我们就需要评估模型的表现。我们可以使用一些指标来衡量模型的表现,例如准确率、精度、召回率、F1 值等等。对于文本生成任务,我们通常使用困惑度(perplexity)作为评估指标。困惑度表示模型对于给定序列的预测能力,计算方法为:

$$\\textperplexity = e^-\\frac1N \\sum_i=1^N \\log p(x_i)$$

其中,$N$ 是序列的长度,$p(x_i)$ 是模型对于第 $i$ 个字符的预测概率。

下面是一个评估模型的示例代码:

def evaluate(rnn, data, prime_str='A', predict_len=100, temperature=0.8): 
    # Initialize hidden state 
    hidden = rnn.init_hidden() 

    # Initialize input sequence 
    prime_input = char_tensor(prime_str) 

    # Generate prime sequence 
    for p in range(len(prime_str) - 1):
         _, hidden = rnn(prime_input[p].unsqueeze(0), hidden) 

    # Initialize predicted sequence 
    predicted = prime_str 

    # Generate predicted sequence 
    for p in range(predict_len): 

        # Forward pass 
        output, hidden = rnn(prime_input[-1].unsqueeze(0), hidden) 

        # Sample from the network as a multinomial distribution 
        output_dist = output.data.view(-1).div(temperature).exp() top_i =         
        torch.multinomial(output_dist, 1)[0] 

        # Add predicted character to string and use as next input 
        predicted_char = all_characters[top_i] 
        predicted += predicted_char 
        prime_input = char_tensor(predicted_char) 

        # Calculate perplexity 
        perplexity = calc_perplexity(rnn, data) 

        return predicted, perplexity 

def calc_perplexity(rnn, data): 

    # Set loss function 
    loss_fn = nn.CrossEntropyLoss() 

    # Initialize variables 
    loss = 0 
    n_chars = 0

    # Iterate over all chunks in data 
    for chunk in data: 

        # Get input and target tensors 
        input_tensor = char_tensor(chunk[:-1]) 
        target_tensor = char_tensor(chunk[1:]) 

        # Initialize hidden state 
        hidden = rnn.init_hidden() 

        # Forward pass 
        output, hidden = rnn(input_tensor, target_tensor, hidden) 

        # Calculate loss 
        loss += loss_fn(output.view(-1, n_characters), target_tensor.view(-1)).item() * len(chunk) 

        # Update number of characters 
        n_chars += len(chunk) - 1 

    # Calculate perplexity 
    perplexity = np.exp(loss / n_chars) 

    return perplexity

我们首先定义了一个 evaluate 函数,该函数接受一个已经训练好的模型、一个初始字符串、一个生成的序列长度和一个温度参数。我们使用初始字符串生成一个起始序列,并利用模型生成一个长度为 predict_len 的序列。然后我们计算该序列的困惑度,并返回生成的序列和困惑度。

计算困惑度的过程在 calc_perplexity 函数中。我们使用交叉熵损失函数计算每个序列的损失,然后累加损失和字符数。最后,我们将损失除以字符数,并使用指数函数计算 perplexity 值。

最后,我们来看一下如何将循环神经网络应用于文本生成任务。对于文本生成任务,我们的目标是根据前面的一段文本生成下一个字符或单词。我们可以使用训练好的循环神经网络模型来预测下一个字符或单词的概率分布,并根据该分布来进行采样。具体来说,我们可以执行以下步骤:

1.给定一个初始字符串和循环神经网络模型。

2.使用初始字符串生成一个起始序列,将其输入到循环神经网络模型中。

3.在模型的输出中,选择一个概率最高的字符作为预测的下一个字符,并将该字符添加到序列的末尾。

4.将预测的字符作为输入,重复步骤 3 直到生成所需长度的序列。

在生成文本时,我们通常会添加一个温度参数来控制生成文本的多样性。温度参数越高,生成的文本越随机;温度参数越低,生成的文本越保守。以下是一个生成文本的示例代码:

def generate_text(rnn, prime_str='A', predict_len=100, temperature=0.8): 
    # Initialize hidden state 
    hidden = rnn.init_hidden() 

    # Initialize input sequence 
    prime_input = char_tensor(prime_str) 

    # Generate prime sequence 
    for p in range(len(prime_str) - 1): 
        _, hidden = rnn(prime_input[p].unsqueeze(0), hidden) 

    # Initialize predicted sequence 
    predicted = prime_str 

    # Generate predicted sequence 
    for p in range(predict_len): 

        # Forward pass 
        output, hidden = rnn(prime_input[-1].unsqueeze(0), hidden) 

        # Sample from the network as a multinomial distribution 
        output_dist = output.data.view(-1).div(temperature).exp() top_i =         
        torch.multinomial(output_dist, 1)[0] 

        # Add predicted character to string and use as next input 
        predicted_char = all_characters[top_i] 
        predicted += predicted_char 
        prime_input = char_tensor(predicted_char) 

    return predicted

我们可以使用该函数来生成指定长度的文本。我们首先使用初始字符串生成一个起始序列,并利用模型生成一个长度为 predict_len 的序列。然后我们使用温度参数对生成的序列进行采样,并返回生成的文本。

在这个过程中,我们还需要一些辅助函数来将字符和单词转换为数字表示。以下是将字符转换为数字表示的辅助函数:

def char_tensor(string): 
    tensor = torch.zeros(len(string)).long() 
    for c in range(len(string)): 
        tensor[c] = all_characters.index(string[c]) 
    return tensor

该函数接受一个字符串作为输入,并将其转换为一个长为字符串长度的整数张量。每个元素都是对应字符在字符表中的索引。

对于单词级别的循环神经网络,我们需要一个不同的辅助函数来将单词转换为数字表示。以下是将单词转换为数字表示的辅助函数:

def word_tensor(word, word_to_ix): 
    tensor = torch.zeros(len(word)).long() 
    for w in range(len(word)): 
        tensor[w] = word_to_ix[word[w]] 
    return tensor

该函数接受一个单词和单词表 word_to_ix 作为输入,并将单词转换为一个长为单词长度的整数张量。每个元素都是对应单词在单词表中的索引。

接下来,我们需要一些训练和评估循环神经网络模型的函数。以下是训练函数的代码:

def train(rnn, criterion, optimizer, train_data, n_epochs=200, print_every=100, plot_every=10): 
    # Initialize losses and perplexities 
    current_loss = 0 
    all_losses = [] 
    current_perplexity = 0 
    all_perplexities = [] 

    # Train loop 
    for epoch in range(1, n_epochs + 1): 
        # Initialize hidden state 
        hidden = rnn.init_hidden() 

        # Shuffle training data 
        random.shuffle(train_data) 

        # Train on each example 
        for i, (input_seq, target_seq) in enumerate(train_data): 
            # Clear gradients 
            optimizer.zero_grad() 

            # Convert input and target sequences to tensors 
            input_tensor = input_seq.unsqueeze(0) 
            target_tensor = target_seq.unsqueeze(0)

            # Forward pass 
            output, hidden = rnn(input_tensor, hidden) 

            # Calculate loss and perplexity 
            loss = criterion(output.view(-1, n_categories), target_tensor.view(-1)) 
            perplexity = torch.exp(loss) 
            
            # Backward pass and optimization step 
            loss.backward() 
            optimizer.step() 

            # Update current loss and perplexity 
            current_loss += loss.item() 
            current_perplexity += perplexity.item() 

            # Print progress 
            if (i + 1) % print_every == 0: 
                print('Epoch [/], Step [/], Loss: :.4f, Perplexity: :.4f' 
                    .format(epoch, n_epochs, i + 1, len(train_data), 
                        current_loss / print_every, current_perplexity / print_every)) 
                current_loss = 0 
                current_perplexity = 0 

        # Add loss and perplexity to list 
        all_losses.append(current_loss) 
        all_perplexities.append(current_perplexity) 

        # Plot losses and perplexities 
        if epoch % plot_every == 0: 
            plot_loss(all_losses, epoch) 
            plot_perplexity(all_perplexities, epoch)

该函数接受循环神经网络模型 rnn、损失函数 criterion、优化器 optimizer、训练数据 train_data、训练周期数 n_epochs、每隔多少步打印一次进度 print_every、每隔多少周期绘制一次损失和困惑度图表 plot_every 作为输入。

在训练过程中,我们首先初始化损失和困惑度的变量 current_loss 和 current_perplexity,然后进行训练循环。在每个训练周期中,我们首先初始化隐藏状态 hidden,然后将训练数据随机打乱。接着,我们对每个训练样本进行训练,具体步骤如下:

1.清除梯度。

2.将输入序列和目标序列转换为张量。

3.前向传播,计算输出序列和新的隐藏状态。

4.计算损失和困惑度。

5.反向传播和优化。

6.更新当前损失和困惑度。

7.如果达到了打印进度的步数,则打印进度,并重置当前损失和困惑度。

在每个训练周期结束后,我们将当前损失和困惑度添加到列表中,并在每隔一定周期数时绘制损失和困惑度图表。

以下是评估函数的代码:

def evaluate(rnn, criterion, data): 
    # Initialize loss and perplexity 
    total_loss = 0 total_perplexity = 0 

    # Loop through data 
    with torch.no_grad(): 
        for input_seq, target_seq in data: 
            # Initialize hidden state 
            hidden = rnn.init_hidden() 

            # Convert input and target sequences to tensors 
            input_tensor = torch.tensor(input_seq, dtype=torch.long).view(-1, 1) 
            target_tensor = torch.tensor(target_seq, dtype=torch.long).view(-1, 1) 

            # Forward pass 
            output, hidden = rnn(input_tensor, hidden) 
            loss = criterion(output, target_tensor) 

            # Update total loss and perplexity 
            total_loss += loss.item() 
            total_perplexity += np.exp(loss.item()) 

# Calculate average loss and perplexity 
avg_loss = total_loss / len(data) 
avg_perplexity = total_perplexity / len(data) 

return avg_loss, avg_perplexity

该函数接受循环神经网络模型 `rnn`、损失函数 `criterion` 和测试数据 `data` 作为输入。在评估过程中,我们初始化损失和困惑度变量 `total_loss` 和 `total_perplexity`,然后对测试数据进行循环。对于每个测试样本,我们首先初始化隐藏状态 `hidden`,然后将输入序列和目标序列转换为张量。接着,我们进行前向传播,计算输出序列和新的隐藏状态,并计算损失和困惑度。最后,我们将总损失和困惑度除以测试样本数,计算平均损失和困惑度。 最后,我们来看一下如何使用训练好的循环神经网络模型进行文本生成。以下是文本生成函数的代码:未完待续。

以上是关于全网最详细使用PyTorch实现循环神经网络的主要内容,如果未能解决你的问题,请参考以下文章

循环神经网络 RNN 从零开始实现 动手学深度学习v2 pytorch

参考《深度学习之PyTorch实战计算机视觉》PDF

DataWhale 动手学深度学习PyTorch版-task3+4+5:文本预处理;语言模型;循环神经网络基础

CNN 卷积神经网络 池化层Pooling 动手学深度学习v2 pytorch

搞定《动手学深度学习》-(李牧)PyTorch版本的所有内容

深度学习为什么选择Pytorch?史上最详细Pytorch入门教程