网站未备案什么意思,网页设计工作心得,wordpress改密码忘记,万网封停慧聪张向东复仇在这篇文章中#xff0c;我们将了解 RNN#xff08;即循环神经网络#xff09;#xff0c;并尝试通过 PyTorch 从头开始实现其中的部分内容。是的#xff0c;这并不完全是从头开始#xff0c;因为我们仍然依赖 PyTorch autograd 来计算梯度并实现反向传播#xff0c…在这篇文章中我们将了解 RNN即循环神经网络并尝试通过 PyTorch 从头开始实现其中的部分内容。是的这并不完全是从头开始因为我们仍然依赖 PyTorch autograd 来计算梯度并实现反向传播但我仍然认为我们也可以从这个实现中收集到有价值的见解。
有关 RNN 的简要介绍性概述我建议您查看上一篇文章其中我们不仅探讨了 RNN 是什么及其工作原理还探讨了如何使用 Keras 实现 RNN 模型。这次我们将使用 PyTorch但采取更实际的方法从头开始构建一个简单的 RNN。
完全免责声明这篇文章很大程度上改编自PyTorch 教程这个 PyTorch 教程。我修改并改变了预处理和训练中涉及的一些步骤。我仍然建议您将其作为补充材料查看。考虑到这一点让我们开始吧。
数据准备
任务是建立一个简单的分类模型可以根据名字正确确定一个人的国籍。更简单地说我们希望能够分辨出特定名称的来源。
下载
我们将使用 PyTorch 教程中的一些标记数据。我们只需输入即可下载
!curl -O https://download.pytorch.org/tutorial/data.zip; unzip data.zip此命令将下载文件并将其解压到当前目录中文件夹名称为data.
现在我们已经下载了所需的数据让我们更详细地看一下数据。首先这是我们需要的依赖项。
import os
import random
from string import ascii_lettersimport torch
from torch import nn
import torch.nn.functional as F
from unidecode import unidecode_ torch.manual_seed(42)
device torch.device(cuda if torch.cuda.is_available() else cpu)我们首先指定一个目录然后尝试打印出其中的所有标签。然后我们可以构建一个字典将语言映射到数字标签。
data_dir ./data/nameslang2label {file_name.split(.)[0]: torch.tensor([i], dtypetorch.long)for i, file_name in enumerate(os.listdir(data_dir))
}我们看到一共有18种语言。我将每个标签包装为张量以便我们可以在训练期间直接使用它们。
lang2label{Czech: tensor([0]),German: tensor([1]),Arabic: tensor([2]),Japanese: tensor([3]),Chinese: tensor([4]),Vietnamese: tensor([5]),Russian: tensor([6]),French: tensor([7]),Irish: tensor([8]),English: tensor([9]),Spanish: tensor([10]),Greek: tensor([11]),Italian: tensor([12]),Portuguese: tensor([13]),Scottish: tensor([14]),Dutch: tensor([15]),Korean: tensor([16]),Polish: tensor([17])}让我们将语言数量存储在某个变量中以便稍后在模型声明中使用它特别是当我们指定最终输出层的大小时。
num_langs len(lang2label)预处理
现在让我们对名称进行预处理。我们首先要用来unidecode标准化所有名称并删除任何锐利符号或类似符号。例如
unidecode(Ślusàrski)Slusarski一旦我们有了解码后的字符串我们就需要将其转换为张量以便模型可以处理它。这可以首先通过构建映射来完成char2idx如下所示。
char2idx {letter: i for i, letter in enumerate(ascii_letters .,:;-)}
num_letters len(char2idx); num_letters59我们看到我们的字符词汇表中共有 59 个标记。这包括空格和标点符号例如 .,:;- . This also means that each name will now be expressed as a tensor of size (num_char, 59) ; in other words, each character will be a tensor of size (59,)。我们现在可以构建一个完成此任务的函数如下所示
def name2tensor(name):tensor torch.zeros(len(name), 1, num_letters)for i, char in enumerate(name):tensor[i][0][char2idx[char]] 1return tensor如果你仔细阅读代码你会发现输出张量的大小是(num_char, 1, 59)这与上面的解释不同。嗯这个额外维度的原因是我们在本例中使用的批量大小为 1。在 PyTorch 中RNN 层期望输入张量的大小为(seq_len, batch_size, input_size)。由于每个名称都有不同的长度因此为了简单起见我们不会对输入进行批处理而只是将每个输入用作单个批处理。有关更详细的讨论请查看此论坛讨论。
name2tensor()让我们使用虚拟输入快速验证函数的输出。
name2tensor(abc)tensor([[[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0.]],[[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0.]],[[0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0.]]])数据集创建
现在我们需要构建包含所有预处理步骤的数据集。让我们将所有解码和转换的张量收集在一个列表中并附上标签。可以从文件名轻松获取标签例如german.txt.
tensor_names []
target_langs []for file in os.listdir(data_dir):with open(os.path.join(data_dir, file)) as f:lang file.split(.)[0]names [unidecode(line.rstrip()) for line in f]for name in names:try:tensor_names.append(name2tensor(name))target_langs.append(lang2label[lang])except KeyError:pass我们可以将其包装在 PyTorchDataset类中但为了简单起见我们只使用一个好的旧for循环将这些数据输入到我们的模型中。由于我们处理的是普通列表因此我们可以轻松地使用sklearn’strain_test_split()将训练数据与测试数据分开。
from sklearn.model_selection import train_test_splittrain_idx, test_idx train_test_split(range(len(target_langs)), test_size0.1, shuffleTrue, stratifytarget_langs
)train_dataset [(tensor_names[i], target_langs[i])for i in train_idx
]test_dataset [(tensor_names[i], target_langs[i])for i in test_idx
]让我们看看我们有多少训练和测试数据。请注意我们使用的 test_size为 0.1。
print(fTrain: {len(train_dataset)})
print(fTest: {len(test_dataset)})Train: 18063
Test: 2007模型
我们将构建两个模型一个简单的 RNN将从头开始构建和一个使用 PyTorch 层的基于 GRU 的模型。
简单循环神经网络
现在我们可以构建我们的模型了。这是一个非常简单的 RNN它采用单个字符张量表示作为输入并产生一些预测和隐藏状态可在下一次迭代中使用。请注意它只是一些在隐藏状态计算期间应用了 sigmoid 非线性的全连接层。
class MyRNN(nn.Module):def __init__(self, input_size, hidden_size, output_size):super(MyRNN, self).__init__()self.hidden_size hidden_sizeself.in2hidden nn.Linear(input_size hidden_size, hidden_size)self.in2output nn.Linear(input_size hidden_size, output_size)def forward(self, x, hidden_state):combined torch.cat((x, hidden_state), 1)hidden torch.sigmoid(self.in2hidden(combined))output self.in2output(combined)return output, hiddendef init_hidden(self):return nn.init.kaiming_uniform_(torch.empty(1, self.hidden_size))我们init_hidden()在每个新批次开始时都会打电话。为了更容易训练和学习我决定使用kaiming_uniform_()来初始化这些隐藏状态。
我们现在可以构建模型并开始训练它。
hidden_size 256
learning_rate 0.001model MyRNN(num_letters, hidden_size, num_langs)
criterion nn.CrossEntropyLoss()
optimizer torch.optim.Adam(model.parameters(), lrlearning_rate)我意识到训练这个模型非常不稳定正如你所看到的损失上下跳跃了很多。尽管如此我不想为难我的 13 英寸 MacBook Pro所以我决定在两个epochs停止。
num_epochs 2
print_interval 3000for epoch in range(num_epochs):random.shuffle(train_dataset)for i, (name, label) in enumerate(train_dataset):hidden_state model.init_hidden()for char in name:output, hidden_state model(char, hidden_state)loss criterion(output, label)optimizer.zero_grad()loss.backward()nn.utils.clip_grad_norm_(model.parameters(), 1)optimizer.step()if (i 1) % print_interval 0:print(fEpoch [{epoch 1}/{num_epochs}], fStep [{i 1}/{len(train_dataset)}], fLoss: {loss.item():.4f})Epoch [1/2], Step [3000/18063], Loss: 0.0390
Epoch [1/2], Step [6000/18063], Loss: 1.0368
Epoch [1/2], Step [9000/18063], Loss: 0.6718
Epoch [1/2], Step [12000/18063], Loss: 0.0003
Epoch [1/2], Step [15000/18063], Loss: 1.0658
Epoch [1/2], Step [18000/18063], Loss: 1.0021
Epoch [2/2], Step [3000/18063], Loss: 0.0021
Epoch [2/2], Step [6000/18063], Loss: 0.0131
Epoch [2/2], Step [9000/18063], Loss: 0.3842
Epoch [2/2], Step [12000/18063], Loss: 0.0002
Epoch [2/2], Step [15000/18063], Loss: 2.5420
Epoch [2/2], Step [18000/18063], Loss: 0.0172现在我们可以测试我们的模型。我们可以查看其他指标但准确性是迄今为止最简单的所以我们就这样吧。
num_correct 0
num_samples len(test_dataset)model.eval()with torch.no_grad():for name, label in test_dataset:hidden_state model.init_hidden()for char in name:output, hidden_state model(char, hidden_state)_, pred torch.max(output, dim1)num_correct bool(pred label)print(fAccuracy: {num_correct / num_samples * 100:.4f}%)Accuracy: 72.2471%该模型的准确率高达 72%。这非常糟糕但考虑到模型非常简单而且我们只训练了两个 epoch 的模型我们可以放松下来享受短暂的快乐因为知道简单的 RNN 模型至少能够学到一些东西。
让我们通过一些具体示例来看看我们的模型表现如何。下面是一个接受字符串作为输入并输出解码预测的函数。
label2lang {label.item(): lang for lang, label in lang2label.items()}def myrnn_predict(name):model.eval()tensor_name name2tensor(name)with torch.no_grad():hidden_state model.init_hidden()for char in tensor_name:output, hidden_state model(char, hidden_state)_, pred torch.max(output, dim1)model.train() return label2lang[pred.item()]我不知道这些名字是否真的在训练或测试集中这些只是我想出的一些随机名称我认为这些名称相当合理。瞧结果是有希望的。
myrnn_predict(Mike)
English
myrnn_predict(Qin)
Chinese
myrnn_predict(Slaveya)
Russian该模型似乎已将所有名称分类为正确的类别
PyTorch GRU
这很酷我可能可以停在这里但我想看看这个自定义模型与使用 PyTorch 层的模型相比如何。对于我们简单的 RNN 来说GRU 可能不太公平但让我们看看它的表现如何。
class GRUModel(nn.Module):def __init__(self, num_layers, hidden_size):super(GRUModel, self).__init__()self.num_layers num_layersself.hidden_size hidden_sizeself.gru nn.GRU(input_sizenum_letters, hidden_sizehidden_size, num_layersnum_layers,)self.fc nn.Linear(hidden_size, num_langs)def forward(self, x):hidden_state self.init_hidden()output, hidden_state self.gru(x, hidden_state)output self.fc(output[-1])return outputdef init_hidden(self):return torch.zeros(self.num_layers, 1, self.hidden_size).to(device)让我们声明模型和与之配套的优化器。请注意我们使用的是两层 GRU它已经比我们当前的 RNN 实现多了一层。
model GRUModel(num_layers2, hidden_sizehidden_size)
optimizer torch.optim.Adam(model.parameters(), lrlearning_rate)
for epoch in range(num_epochs):random.shuffle(train_dataset)for i, (name, label) in enumerate(train_dataset):output model(name)loss criterion(output, label)optimizer.zero_grad()loss.backward()optimizer.step()if (i 1) % print_interval 0:print(fEpoch [{epoch 1}/{num_epochs}], fStep [{i 1}/{len(train_dataset)}], fLoss: {loss.item():.4f})Epoch [1/2], Step [3000/18063], Loss: 1.8497
Epoch [1/2], Step [6000/18063], Loss: 0.4908
Epoch [1/2], Step [9000/18063], Loss: 1.0299
Epoch [1/2], Step [12000/18063], Loss: 0.0855
Epoch [1/2], Step [15000/18063], Loss: 0.0053
Epoch [1/2], Step [18000/18063], Loss: 2.6417
Epoch [2/2], Step [3000/18063], Loss: 0.0004
Epoch [2/2], Step [6000/18063], Loss: 0.0008
Epoch [2/2], Step [9000/18063], Loss: 0.1446
Epoch [2/2], Step [12000/18063], Loss: 0.2125
Epoch [2/2], Step [15000/18063], Loss: 3.7883
Epoch [2/2], Step [18000/18063], Loss: 0.4862训练一开始看起来比较稳定但我们确实在第二个时期结束时看到了奇怪的跳跃。部分原因是我没有对此 GRU 模型使用梯度裁剪并且应用裁剪后我们可能会看到更好的结果。
让我们看看这个模型的准确性。
num_correct 0model.eval()with torch.no_grad():for name, label in test_dataset:output model(name)_, pred torch.max(output, dim1)num_correct bool(pred label)print(fAccuracy: {num_correct / num_samples * 100:.4f}%)Accuracy: 81.4150%我们得到的这个模型的准确率约为 80%。这比我们的简单 RNN 模型要好这在某种程度上是预料之中的因为它有一个附加层并且使用了更复杂的 RNN 单元模型。
让我们看看这个模型如何预测给定的一些原始名称字符串。
def pytorch_predict(name):model.eval()tensor_name name2tensor(name)with torch.no_grad():output model(tensor_name)_, pred torch.max(output, dim1)model.train()return label2lang[pred.item()]
pytorch_predict(Jake)
English
pytorch_predict(Qin)
Chinese
pytorch_predict(Fernando)
Spanish
pytorch_predict(Demirkan)
Russian最后一个很有趣因为这是我一位土耳其好朋友的名字。该模型显然无法告诉我们这个名字是土耳其语因为它没有看到任何标记为土耳其语的数据点但它告诉我们这个名字可能属于它所训练的 18 个标签中的哪个国籍。这显然是错误的但在某些方面也许相差并不远。例如至少它没有说日语。对于该模型来说这也不是完全公平的游戏因为有许多名字可能被描述为跨国的也许有一个俄罗斯人的名字叫 Demirkan。
结论
通过实现这个 RNN我学到了很多关于 RNN 的知识。诚然它很简单并且与 PyTorch 基于层的方法有所不同因为它需要我们手动循环每个字符但它的低级性质迫使我更多地思考张量维度以及具有张量维度的目的。隐藏状态和输出之间的划分。这也很好地提醒了我们 RNN 是如何难以训练的。
在接下来的文章中我们将研究序列到序列模型简称 seq2seq。自从听说 seq2seq 以来我就对将一种数据形式转换为另一种数据形式的力量着迷。尽管由于本地机器的限制这些模型无法在 CPU 上进行实际训练但我认为实现它们本身将是一个令人兴奋的挑战。 本博文译自Jake Tae的博文。