0


自然语言处理系列(二)——使用RNN搭建字符级语言模型

目录

一、任务背景

本文的任务主要来源于PyTorch的官方教程,即给定各国人名的数据集,你需要训练出一个RNN,它能够根据输入的人名来判断这个人来自哪个国家(分类任务)。

数据集是一个

  1. names

文件夹,里面包含了

  1. 18
  2. 18
  3. 18 个文本文档,均以
  1. [Language].txt

命名。每个文本文档中,每一行都是该语种下的一个(常见)人名。

需要注意的是,官方给的数据集疑似存在错误(爬虫没爬干净) ,在

  1. Russian.txt

文件中,第

  1. 7941
  2. 7964
  3. 7941\sim7964
  4. 79417964 行出现了
  1. To The First Page

字样,很显然这不是一个人名。此外,第

  1. 4395
  2. ,
  3. 5236
  4. ,
  5. 5255
  6. 4395,5236,5255
  7. 4395,5236,5255 行的人名均以
  1. ,

结尾(我想俄罗斯人的名字应该不会以逗号结尾吧?)。

博主已经帮大家修正了这个数据集,下载地址(所需积分0)。

不同于官方教程,在本篇文章中,博主会根据自己的理解重构代码,使其变得通俗易懂。

二、数据预处理

首先,我们需要构造一个字典,它的格式为:

  1. {language: [names ...]}

因为人名均由Unicode字符组成,我们需要先将其转化为ASCII字符:

  1. import unicodedata
  2. import string
  3. # 转化后的人名由大小写字母和空格字符,单引号字符组成
  4. all_letters = string.ascii_letters +" '"defunicodeToAscii(s):return''.join(c for c in unicodedata.normalize('NFD', s)if unicodedata.category(c)!='Mn'and c in all_letters)print(unicodeToAscii('Ślusàrski'))# Slusarski

构造字典:

  1. import os
  2. filenames = os.listdir('names')
  3. data =dict()for filename in filenames:# 注意需要以utf-8格式打开withopen(f'names/{filename}', encoding='utf-8')as f:# 需要去掉filename中的.txt后缀
  4. data[filename[:-4]]=[unicodeToAscii(name)for name in f.readlines()]
  5. all_categories =list(data.keys())print(all_categories)# ['Arabic', 'Chinese', 'Czech', 'Dutch', 'English', 'French', 'German', 'Greek', 'Irish', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'Spanish', 'Vietnamese']print(data['Arabic'][:6])# ['Khoury', 'Nahas', 'Daher', 'Gerges', 'Nazari', 'Maalouf']

从下面的输出结果可以看出,数据集一共有 18 类,并且每一类中的数据个数也都不同:

  1. print(len(all_categories))# 18print([len(data[category])for category in all_categories])# [2000, 268, 519, 297, 3668, 277, 724, 203, 232, 709, 991, 94, 139, 74, 9384, 100, 298, 73]

神经网络无法直接处理字母,因此我们需要先将字母转化成对应的 One-Hot 向量:

  1. import torch
  2. import torch.nn.functional as F
  3. defletterToTensor(letter):# 获取letterall_letters中的索引
  4. letter_idx = torch.tensor(all_letters.index(letter))return F.one_hot(letter_idx, num_classes=len(all_letters))
  5. r = letterToTensor("c")print(r)# tensor([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])

向量长度为 54,这是因为有26个小写字母+26个大写字母+空格字符+单引号字符。

接下来我们需要将完整的单词(人名)编码成张量。单词中的每一个字母代表一个时间步,因此一个完整的单词可以看作是一个序列。由于单词的长度各不相同,因此我们把一个单词看作一个 batch,从而单词应是形状为

  1. (sequence_length, batch_size, features) = (sequence_length, 1, 54)

的张量。

  1. defnameToTensor(name):
  2. result = torch.zeros(len(name),len(all_letters))for i inrange(len(name)):
  3. result[i]= letterToTensor(name[i])return result.unsqueeze(1)print(nameToTensor('ab'))# 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., 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.]]])print(nameToTensor('abcd').size())# torch.Size([4, 1, 54])

因为我们已经将一个单词视为了一个batch,所以不再使用DataLoader去构造迭代器,而是采用随机采样的方法去抽取数据进行训练。

三、模型搭建与训练

我们使用PyTorch中的

  1. nn.RNN

模块来搭建单隐层单向RNN:

  1. import torch.nn as nn
  2. classRNN(nn.Module):def__init__(self):super().__init__()
  3. self.rnn = nn.RNN(
  4. input_size=54,# 128是随便选的,也可以选择其他数值
  5. hidden_size=128,)# 18分类任务,所以最后一层有18个神经元
  6. self.out = nn.Linear(128,18)defforward(self, x):# None代表全零初始化隐状态
  7. output, h_n = self.rnn(x,None)# output[-1]是最后时刻输出的隐状态,等同于h_n[0]return self.out(output[-1])

考虑到不同类别的样本个数不同(相差较大),并且我们已经将一个单词视为了一个batch,因此使用DataLoader会显得较为困难。这里保持和官方教程一样的做法,即每次随机从数据集

  1. data

中抽取一个

  1. category

,再从

  1. category

中随机抽取一个

  1. name

投喂到RNN中。

  1. import random
  2. defrandom_sample():
  3. category = random.choice(all_categories)
  4. name = random.choice(data[category])return category, name
  5. print(random_sample())# ('Irish', "O'Kane")

我们选择在GPU上进行训练,选择交叉熵损失和SGD优化器:

  1. LR =1e-3# 学习率
  2. N_ITERS =10**5# 训练多少个iteration
  3. device ='cuda'if torch.cuda.is_available()else'cpu'
  4. rnn = RNN()
  5. rnn.to(device)
  6. criterion = nn.CrossEntropyLoss()
  7. optimizer = torch.optim.SGD(rnn.parameters(), lr=LR, momentum=0.9)
  8. train_loss =[]deftrain(model, criterion, optimizer):
  9. model.train()
  10. avg_loss =0for iteration inrange(N_ITERS):
  11. category, name = random_sample()# name转化成数字
  12. X = nameToTensor(name).to(device)# 因为output的形状是(1,18),所以target的形状必须为(1,)而非标量
  13. target = torch.tensor([all_categories.index(category)]).to(device)# 正向传播
  14. output = model(X)
  15. loss = criterion(output, target)
  16. avg_loss += loss
  17. # 反向传播
  18. optimizer.zero_grad()
  19. loss.backward()
  20. optimizer.step()# 每隔1000iteration输出一次平均loss并保存if(iteration +1)%1000==0:
  21. avg_loss /=1000
  22. train_loss.append(avg_loss.item())print(f"Iteration: [{iteration +1}/{N_ITERS}] | Train Loss: {avg_loss:.4f}")
  23. avg_loss =0

训练完成后,绘制损失函数的曲线:

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. plt.plot(np.arange(1, N_ITERS +1,1000), train_loss)
  4. plt.ylabel('Train Loss')
  5. plt.xlabel('Iteration')
  6. plt.show()

为了保证结果的可复现性,我们需要设置全局种子:

  1. defsetup_seed(seed):
  2. random.seed(seed)# random库设置种子
  3. np.random.seed(seed)# numpy库设置种子
  4. torch.manual_seed(seed)# Pytorch-CPU设置种子
  5. torch.cuda.manual_seed(seed)# 为当前GPU设置种子
  6. torch.cuda.manual_seed_all(seed)# 为所有GPU设置种子

设置种子为 42,损失曲线变化图如下:

在这里插入图片描述

从上图可以看出,模型损失在第

  1. 70000
  2. 70000
  3. 70000 iteration 左右时达到了最小。不过这里为了方便起见,我们采用最后得到的模型用来测试。

四、模型测试

在训练过程中,我们一共训练了

  1. 100000
  2. 100000
  3. 100000 Iteration,每个 Iteration 中只有一个样本。在测试阶段,我们随机抽取
  4. 10000
  5. 10000
  6. 10000 个样本来绘制混淆矩阵:
  1. from sklearn.metrics import ConfusionMatrixDisplay
  2. deftest(model):
  3. model.eval()
  4. y_true, y_pred =[],[]for _ inrange(10000):
  5. category, name = random_sample()# 获取该样本的真实标签对应的下标
  6. true_idx = all_categories.index(category)
  7. y_true.append(true_idx)# 获取预测标签对应的下标
  8. X = nameToTensor(name).to(device)
  9. output = model(X)
  10. y_pred.append(output.argmax().item())# 绘制混淆矩阵
  11. ConfusionMatrixDisplay.from_predictions(y_true,
  12. y_pred,
  13. labels=np.arange(18),
  14. display_labels=all_categories,
  15. xticks_rotation='vertical',
  16. normalize='true',
  17. include_values=False)
  18. plt.show()

最终结果:

在这里插入图片描述

从混淆矩阵的结果可以看出:

  • Korean 容易被误判成 Chinese,Czech 容易被误判成 Polish,German 容易被误判成 Dutch;
  • English、German、Czech 不容易被识别.

附录:完整代码


运行环境:

  • 系统:Ubuntu 20.04
  • GPU:RTX 3090
  • PyTorch版本:1.10
  • Python版本:3.8
  • Cuda:11.3

需要注意的是,在 Linux 系统中,

  1. os.listdir()

的返回结果与 Windows 系统不同,因此即使随机种子相同,也有可能产生不同的实验结果。


  1. import os
  2. import string
  3. import random
  4. import unicodedata
  5. import torch
  6. import torch.nn as nn
  7. import torch.nn.functional as F
  8. import numpy as np
  9. import matplotlib.pyplot as plt
  10. from sklearn.metrics import ConfusionMatrixDisplay
  11. classRNN(nn.Module):def__init__(self):super().__init__()
  12. self.rnn = nn.RNN(input_size=54, hidden_size=128)
  13. self.out = nn.Linear(128,18)defforward(self, x):
  14. output, h_n = self.rnn(x,None)return self.out(output[-1])defunicodeToAscii(s):return''.join(c for c in unicodedata.normalize('NFD', s)if unicodedata.category(c)!='Mn'and c in all_letters)defletterToTensor(letter):
  15. letter_idx = torch.tensor(all_letters.index(letter))return F.one_hot(letter_idx, num_classes=len(all_letters))defnameToTensor(name):
  16. result = torch.zeros(len(name),len(all_letters))for i inrange(len(name)):
  17. result[i]= letterToTensor(name[i])return result.unsqueeze(1)defrandom_sample():
  18. category = random.choice(all_categories)
  19. name = random.choice(data[category])return category, name
  20. deftrain(model, critertion, optimizer):
  21. model.train()
  22. avg_loss =0for iteration inrange(N_ITERS):
  23. category, name = random_sample()
  24. X = nameToTensor(name).to(device)
  25. target = torch.tensor([all_categories.index(category)]).to(device)
  26. output = model(X)
  27. loss = criterion(output, target)
  28. avg_loss += loss
  29. optimizer.zero_grad()
  30. loss.backward()
  31. optimizer.step()if(iteration +1)%1000==0:
  32. avg_loss /=1000
  33. train_loss.append(avg_loss.item())print(f"Iteration: [{iteration +1}/{N_ITERS}] | Train Loss: {avg_loss:.4f}")
  34. avg_loss =0deftest(model):
  35. model.eval()
  36. y_true, y_pred =[],[]for _ inrange(10000):
  37. category, name = random_sample()
  38. true_idx = all_categories.index(category)
  39. y_true.append(true_idx)
  40. X = nameToTensor(name).to(device)
  41. output = model(X)
  42. y_pred.append(output.argmax().item())
  43. ConfusionMatrixDisplay.from_predictions(y_true,
  44. y_pred,
  45. labels=np.arange(18),
  46. display_labels=all_categories,
  47. xticks_rotation='vertical',
  48. normalize='true',
  49. include_values=False)
  50. plt.show()defsetup_seed(seed):
  51. random.seed(seed)
  52. np.random.seed(seed)
  53. torch.manual_seed(seed)
  54. torch.cuda.manual_seed(seed)
  55. torch.cuda.manual_seed_all(seed)""" Data preprocessing """
  56. all_letters = string.ascii_letters +" '"
  57. filenames = os.listdir('names')
  58. data =dict()for filename in filenames:withopen(f'names/{filename}', encoding='utf-8')as f:
  59. data[filename[:-4]]=[unicodeToAscii(name)for name in f.readlines()]
  60. all_categories =list(data.keys())""" Model building and training """
  61. setup_seed(42)
  62. LR =1e-3
  63. N_ITERS =10**5
  64. device ='cuda'if torch.cuda.is_available()else'cpu'
  65. rnn = RNN()
  66. rnn.to(device)
  67. criterion = nn.CrossEntropyLoss()
  68. optimizer = torch.optim.SGD(rnn.parameters(), lr=LR, momentum=0.9)
  69. train_loss =[]
  70. train(rnn, criterion, optimizer)
  71. plt.plot(np.arange(1, N_ITERS +1,1000), train_loss)
  72. plt.ylabel('Train Loss')
  73. plt.xlabel('Iteration')
  74. plt.show()""" Testing """
  75. test(rnn)

如果这篇文章有帮助到你还请麻烦你点一个免费的赞,这将是我创作的最大动力!


本文转载自: https://blog.csdn.net/raelum/article/details/125381732
版权归原作者 raelum 所有, 如有侵权,请联系我们删除。

“自然语言处理系列(二)——使用RNN搭建字符级语言模型”的评论:

还没有评论