RNN完成姓名分类

  • A+
所属分类:深度学习
摘要这一篇文章会介绍一下关于使用RNN来完成name的分类,区分名字是属于哪个国家的。这一篇改编自Pytorch官方文档的一个例子。主要目的是熟悉一下RNN。

简介

这一篇会介绍使用RNN, 更确切的说是GRU来完成姓名的分类。原文是来自Pytorch的官方的例子中,链接如下, CLASSIFYING NAMES WITH A CHARACTER-LEVEL RNN, 可以在这里下载到需要使用的数据。也可以从下面这个链接中下载所需要的数据 : 数据下载链接.

本文的主要希望完成的任务如下(来自上面的原文):

Specifically, we'll train on a few thousand surnames from 18 languages of origin, and predict which language a name is from based on the spelling:

  1. $ python predict.py Hinton
  2. (-0.47) Scottish
  3. (-1.52) English
  4. (-3.57) Irish
  5. $ python predict.py Schmidhuber
  6. (-0.19) German
  7. (-2.48) Czech
  8. (-2.68) Dutch

下面看一下具体的实现的过程。

Pytorch具体实现

关于实现部分,前面的数据预处理部分和上面原文是一样的我只是修改了原文的网络的结构。使用了Pytorch中自带的GRU的单元来实现了网络的编写

导入需要的库

  1. import glob # 用于查找符合规则的文件名
  2. import os
  3. import unicodedata
  4. import string
  5. import torch
  6. import torch.nn as nn
  7. import torch.optim as optim

导入数据(数据准备阶段)

首先定义函数,用来找出目录下所有存放名字的文件。

  1. def findFiles(path):
  2.     return glob.glob(path)
  3. findFiles('./data/names/*.txt')
  4. """
  5. ['./data/names\\Arabic.txt',
  6.  './data/names\\Chinese.txt',
  7.  './data/names\\Czech.txt',
  8.  './data/names\\Dutch.txt',
  9.  './data/names\\English.txt',
  10.  './data/names\\French.txt',
  11.  './data/names\\German.txt',
  12.  './data/names\\Greek.txt',
  13.  './data/names\\Irish.txt',
  14.  './data/names\\Italian.txt',
  15.  './data/names\\Japanese.txt',
  16.  './data/names\\Korean.txt',
  17.  './data/names\\Polish.txt',
  18.  './data/names\\Portuguese.txt',
  19.  './data/names\\Russian.txt',
  20.  './data/names\\Scottish.txt',
  21.  './data/names\\Spanish.txt',
  22.  './data/names\\Vietnamese.txt']
  23. """

接着定义所有字母的集合, 之后一个字母就会转换为相应维度的一个向量。(相当于是one-hot表示)

  1. all_letters = string.ascii_letters + " .,;'" #所有的字母和标点
  2. n_letters = len(all_letters)
  3. all_letters
  4. # "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'"

定义编码转换的函数,将Unicode转换为ASCII。

  1. # Turn a Unicode string to plain ASCII
  2. # Thanks to https://stackoverflow.com/a/518232/2809427
  3. def unicodeToAscii(s):
  4.     return ''.join(
  5.         c for c in unicodedata.normalize('NFD', s)
  6.         if unicodedata.category(c) != 'Mn'
  7.         and c in all_letters
  8.     )
  9. print(unicodeToAscii('Ślusàrski'))
  10. # Slusarski

接下来将每个文件中的数据导入,all_categories保存18种语言的名字. category_lines为字典类型, 分别保存每种类型语言下的名字。

  1. category_lines = {} # 每个语言下的名字
  2. all_categories = [] # 保存所有的语言的名字
  3. # 读文件, 返回文件中的每一个
  4. def readLines(filename):
  5.     # 读文件, 去空格, 按换行进行划分
  6.     lines = open(filename, encoding='utf-8').read().strip().split('\n')
  7.     return [unicodeToAscii(line) for line in lines]
  8. # 顺序读取每一个文件, 保存其中的姓名
  9. for filename in findFiles('./data/names/*.txt'):
  10.     # basename返回基础的文件名
  11.     # splitext将文件名和后缀分开
  12.     category = os.path.splitext(os.path.basename(filename))[0]
  13.     all_categories.append(category)
  14.     lines = readLines(filename)
  15.     category_lines[category]=lines

下面简单看一下变量中的数据

  1. n_categories = len(all_categories)
  2. # 一共有18种语言的名字
  3. len(category_lines)
  4. # 18
  5. # 查看某种语言的名字
  6. category_lines['English'][:5]
  7. # ['Abbas', 'Abbey', 'Abbott', 'Abdi', 'Abel']

将Name转为Tensor

To represent a single letter, we use a "one-hot vector" of size <1 x n_letters>. A one-hot vector is filled with 0s except for a 1 at index of the current letter, e.g. "b" = <0 1 0 0 0 ...>.

  • 最终输入网络的大小为, <line_length x 1 x n_letters>
  • 相当于, 每次输入一个字母, 一个名字就相当于是一串数字, 所以是变长的.
  • 中间的1相当于时网络的batch_size=1

后面会看一个具体的例子(Mike的例子)。我们首先定义一个函数, 用来将name转换为Tensor。

  1. # Find letter index from all_letters, e.g. "a"=0
  2. def letterToIndex(letter):
  3.     # 返回字母在字母表中的位置
  4.     return all_letters.find(letter)
  5. # 将整个名字转为<line_length x 1 x n_letters>
  6. def lineToTensor(line):
  7.     tensor = torch.zeros(len(line), 1, n_letters)
  8.     for li, letter in enumerate(line):
  9.         tensor[li][0][letterToIndex(letter)] = 1
  10.     return tensor

例如,我们将Mike, 会转换为一个4157的向量, 如下所示。

  1. # 例如Mike会转换为 4(四个字母)*1(batch_size=1)*57(每个字母有57个维度的特征) 的向量
  2. lineToTensor('Mike')
RNN完成姓名分类

定义网络

接下来,我们就可以来定义网络。这里是和原链接有所不同

  1. class RNN_NAME(nn.Module):
  2.     def __init__(self, input_size, hidden_size, output_size, layers, batch_size):
  3.         super(RNN_NAME, self).__init__()
  4.         # 这里的input_size相当于是特征的个数, 这里即是所有字母的个数57
  5.         self.input_size = input_size
  6.         # 这里相当于是rnn输出的维数
  7.         self.hidden_size = hidden_size
  8.         # 这里是分类的个数, 这里相当于是18, 一共有18类
  9.         self.output_size = output_size
  10.         # 这里是rnn的层数
  11.         self.layers = layers
  12.         # batchsize的个数
  13.         self.batch_size = batch_size
  14.         self.gru = nn.GRU(input_size = self.input_size, hidden_size = self.hidden_size, num_layers = self.layers)
  15.         self.FC = nn.Linear(self.hidden_size, self.output_size)
  16.     def init_hidden(self):
  17.         return torch.zeros(self.layers, self.batch_size, self.hidden_size)
  18.     def forward(self, x):
  19.         self.batch_size = x.size(1)
  20.         self.hidden = self.init_hidden() # 初始化memory的内容
  21.         rnn_out, self.hidden = self.gru(x,self.hidden)
  22.         out = self.FC(rnn_out[-1])
  23.         return out

接着进行网络的初始化并输入一个测试数据, 看一下整个编写是否正确.

  1. layers = 3
  2. input_size = n_letters
  3. hidden_size = 128
  4. output_size = len(all_categories) # 18类
  5. batch_size = 1 # 因为这里name的长度不相同, 所以将batch_size设为1
  6. # 网络初始化
  7. rnn_name = RNN_NAME(input_size, hidden_size, output_size, layers, batch_size)
  8. # 测试网络输入输出
  9. input_data = lineToTensor('Mike')
  10. out_data = rnn_name(input_data)
  11. out_data.size()
  12. # torch.Size([1, 18])

可以看到是正确的,接下来就准备一下训练的数据。

准备训练数据

这里主要定义一个函数,为了方便之后的训练

  1. # 打印最后显示的结果
  2. def categoryFromOutput(output):
  3.     top_n, top_i = output.topk(1)
  4.     category_i = top_i[0].item()
  5.     return all_categories[category_i], category_i
  6. # Get a random training example
  7. import random
  8. def randomChoice(l):
  9.     # 随机返回一个l中的分类
  10.     return l[random.randint(0,len(l)-1)]
  11. def randomTrainingExample():
  12.     """随机挑选一种语言的一个名字, 用来产生训练需要的数据
  13.     """
  14.     category = randomChoice(all_categories) # 随机选一个语言
  15.     line = randomChoice(category_lines[category]) # 随机从这个语言里选一个名字
  16.     category_tensor = torch.tensor([all_categories.index(category)]).long() # 将分类转为Tensor
  17.     line_tensor = lineToTensor(line) # 将名字转为Tensor
  18.     return category, line, category_tensor, line_tensor
  19. for i in range(10):
  20.     category, line, category_tensor, line_tensor = randomTrainingExample()
  21.     print('category = ', category, '/ line = ', line)
  22. """
  23. category =  Scottish / line =  Aitken
  24. category =  Vietnamese / line =  Lieu
  25. category =  Vietnamese / line =  Kim
  26. category =  Italian / line =  Guidi
  27. category =  German / line =  Hoefler
  28. category =  Chinese / line =  Pei
  29. category =  Vietnamese / line =  an
  30. category =  French / line =  De la fontaine
  31. category =  Dutch / line =  Tunneson
  32. category =  Polish / line =  Nowak
  33. """

我们之后训练的时候,会使用randomTrainingExample来获取训练的数据, 每次会获取category(种类), line(name的拼写), category_tensor(种类的tensor), line_tensor(name的tensor). 如下面的例子。

RNN完成姓名分类

我们在定义一个函数用来计算准确率

  1. def get_accuracy(logit, target):
  2.     corrects = (torch.max(logit, 1)[1].view(target.size()).data == target.data).sum()
  3.     accuracy = 100.0 * corrects/batch_size
  4.     return accuracy.item()

这样,所有都准备好,就可以开始模型的训练了。

模型的训练

关于模型的训练,因为每个name的长度不相同,所以batch_size只能设置为1。所以我在这里,没有每一次计算loss后都进行反向传播和系数优化,而是每print_every次进行反向传播和优化(具体可以看代码中反向传播和优化代码的位置),这样会得到比较好的效果,如果每次都反向传播效果会不稳定。

  1. criterion = nn.CrossEntropyLoss()
  2. optimizer = optim.Adam(rnn_name.parameters(),lr=0.005)
  3. N_EPHOCS = 70
  4. # 共有20074个数据
  5. n_iters = 2000 # 训练n_iters个名字
  6. print_every = 200 # 每print_every轮打印一次结果, 并传递一次误差, 更新系数
  7. for epoch in range(N_EPHOCS):
  8.     train_running_loss = 0.0
  9.     loss = torch.tensor([0.0]).float()
  10.     train_count = 0
  11.     rnn_name.train()
  12.     # trainging round
  13.     for iter in range(1, n_iters+1):
  14.         # 获取样本
  15.         category, line, category_tensor, line_tensor = randomTrainingExample()
  16.         # 进行训练    
  17.         optimizer.zero_grad()
  18.         # reset hidden states
  19.         rnn_name.hidden = rnn_name.init_hidden()
  20.         # forward+backward+optimize
  21.         outputs = rnn_name(line_tensor)
  22.         guess, guess_i = categoryFromOutput(outputs)
  23.         if guess == category:
  24.             train_count = train_count + 1
  25.         loss = loss + criterion(outputs, category_tensor)
  26.         if iter % print_every == 0:
  27.             correct = '✓' if guess == category else '✗ ({})'.format(category)
  28.             print('{} /{} {}'.format(line, guess, correct))
  29.             # 进行反向传播(print_every次传一次误差)
  30.             loss.backward()
  31.             optimizer.step()
  32.             train_running_loss = train_running_loss + loss.detach().item()
  33.             loss = torch.tensor([0.0]).float()
  34.         # 查看准确率
  35.         train_acc = train_count / n_iters * 100
  36.     print('=======')
  37.     print('Epoch : {:0>2d} | Loss : {:<6.4f} | Train Accuracy : {:<6.2f}%'.format(epoch, train_running_loss/n_iters, train_acc))
  38.     print('=======')
RNN完成姓名分类

最终,在训练集上的准确率可以达到90%以上,我们看一下测试集的准确率。

模型的验证

  1. test_count = 0
  2. test_num = 500 # 测试500个名字
  3. rnn_name.eval()
  4. for test_iter in range(1, test_num+1):
  5.     category, line, category_tensor, line_tensor = randomTrainingExample()
  6.     outputs = rnn_name(line_tensor)
  7.     guess, guess_i = categoryFromOutput(outputs)
  8.     if guess == category:
  9.         test_count = test_count + 1
  10. print('Test Accuracy : {:<6.4f}%'.format(test_count/test_num*100))
  11. # Test Accuracy : 91.8000%

我们随机挑选了500个name进行测试,准确率可以达到90%,这里其实做得不够完美,因为这样训练集和测试集是会有重叠得部分的。

我们单独找一个name,来做一下验证。

  1. input_data = lineToTensor('WMN')
  2. out_data = rnn_name(input_data)
  3. categoryFromOutput(out_data)
  4. # ('Chinese', 1)

嗯,很迷。这个例子也就大概看一下GRU的实现和RNN在文字处理上的一个应用。我看了一下样本的data,其实data也不是很好。所以,就当熟悉一下如何来书写代码吧。

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: