文章目录(Table of Contents)
简介
这一篇会介绍使用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:
- $ python predict.py Hinton
- (-0.47) Scottish
- (-1.52) English
- (-3.57) Irish
- $ python predict.py Schmidhuber
- (-0.19) German
- (-2.48) Czech
- (-2.68) Dutch
下面看一下具体的实现的过程。
Pytorch具体实现
关于实现部分,前面的数据预处理部分和上面原文是一样的。我只是修改了原文的网络的结构。使用了Pytorch中自带的GRU的单元来实现了网络的编写。
导入需要的库
- import glob # 用于查找符合规则的文件名
- import os
- import unicodedata
- import string
- import torch
- import torch.nn as nn
- import torch.optim as optim
导入数据(数据准备阶段)
首先定义函数,用来找出目录下所有存放名字的文件。
- def findFiles(path):
- return glob.glob(path)
- findFiles('./data/names/*.txt')
- """
- ['./data/names\\Arabic.txt',
- './data/names\\Chinese.txt',
- './data/names\\Czech.txt',
- './data/names\\Dutch.txt',
- './data/names\\English.txt',
- './data/names\\French.txt',
- './data/names\\German.txt',
- './data/names\\Greek.txt',
- './data/names\\Irish.txt',
- './data/names\\Italian.txt',
- './data/names\\Japanese.txt',
- './data/names\\Korean.txt',
- './data/names\\Polish.txt',
- './data/names\\Portuguese.txt',
- './data/names\\Russian.txt',
- './data/names\\Scottish.txt',
- './data/names\\Spanish.txt',
- './data/names\\Vietnamese.txt']
- """
接着定义所有字母的集合, 之后一个字母就会转换为相应维度的一个向量。(相当于是one-hot表示)
- all_letters = string.ascii_letters + " .,;'" #所有的字母和标点
- n_letters = len(all_letters)
- all_letters
- # "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,;'"
定义编码转换的函数,将Unicode转换为ASCII。
- # Turn a Unicode string to plain ASCII
- # Thanks to https://stackoverflow.com/a/518232/2809427
- def unicodeToAscii(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
接下来将每个文件中的数据导入,all_categories保存18种语言的名字. category_lines为字典类型, 分别保存每种类型语言下的名字。
- category_lines = {} # 每个语言下的名字
- all_categories = [] # 保存所有的语言的名字
- # 读文件, 返回文件中的每一个
- def readLines(filename):
- # 读文件, 去空格, 按换行进行划分
- lines = open(filename, encoding='utf-8').read().strip().split('\n')
- return [unicodeToAscii(line) for line in lines]
- # 顺序读取每一个文件, 保存其中的姓名
- for filename in findFiles('./data/names/*.txt'):
- # basename返回基础的文件名
- # splitext将文件名和后缀分开
- category = os.path.splitext(os.path.basename(filename))[0]
- all_categories.append(category)
- lines = readLines(filename)
- category_lines[category]=lines
下面简单看一下变量中的数据
- n_categories = len(all_categories)
- # 一共有18种语言的名字
- len(category_lines)
- # 18
- # 查看某种语言的名字
- category_lines['English'][:5]
- # ['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。
- # Find letter index from all_letters, e.g. "a"=0
- def letterToIndex(letter):
- # 返回字母在字母表中的位置
- return all_letters.find(letter)
- # 将整个名字转为<line_length x 1 x n_letters>
- def lineToTensor(line):
- tensor = torch.zeros(len(line), 1, n_letters)
- for li, letter in enumerate(line):
- tensor[li][0][letterToIndex(letter)] = 1
- return tensor
例如,我们将Mike, 会转换为一个4157的向量, 如下所示。
- # 例如Mike会转换为 4(四个字母)*1(batch_size=1)*57(每个字母有57个维度的特征) 的向量
- lineToTensor('Mike')
定义网络
接下来,我们就可以来定义网络。这里是和原链接有所不同。
- class RNN_NAME(nn.Module):
- def __init__(self, input_size, hidden_size, output_size, layers, batch_size):
- super(RNN_NAME, self).__init__()
- # 这里的input_size相当于是特征的个数, 这里即是所有字母的个数57
- self.input_size = input_size
- # 这里相当于是rnn输出的维数
- self.hidden_size = hidden_size
- # 这里是分类的个数, 这里相当于是18, 一共有18类
- self.output_size = output_size
- # 这里是rnn的层数
- self.layers = layers
- # batchsize的个数
- self.batch_size = batch_size
- self.gru = nn.GRU(input_size = self.input_size, hidden_size = self.hidden_size, num_layers = self.layers)
- self.FC = nn.Linear(self.hidden_size, self.output_size)
- def init_hidden(self):
- return torch.zeros(self.layers, self.batch_size, self.hidden_size)
- def forward(self, x):
- self.batch_size = x.size(1)
- self.hidden = self.init_hidden() # 初始化memory的内容
- rnn_out, self.hidden = self.gru(x,self.hidden)
- out = self.FC(rnn_out[-1])
- return out
接着进行网络的初始化,并输入一个测试数据, 看一下整个编写是否正确.
- layers = 3
- input_size = n_letters
- hidden_size = 128
- output_size = len(all_categories) # 18类
- batch_size = 1 # 因为这里name的长度不相同, 所以将batch_size设为1
- # 网络初始化
- rnn_name = RNN_NAME(input_size, hidden_size, output_size, layers, batch_size)
- # 测试网络输入输出
- input_data = lineToTensor('Mike')
- out_data = rnn_name(input_data)
- out_data.size()
- # torch.Size([1, 18])
可以看到是正确的,接下来就准备一下训练的数据。
准备训练数据
这里主要定义一个函数,为了方便之后的训练
- # 打印最后显示的结果
- def categoryFromOutput(output):
- top_n, top_i = output.topk(1)
- category_i = top_i[0].item()
- return all_categories[category_i], category_i
- # Get a random training example
- import random
- def randomChoice(l):
- # 随机返回一个l中的分类
- return l[random.randint(0,len(l)-1)]
- def randomTrainingExample():
- """随机挑选一种语言的一个名字, 用来产生训练需要的数据
- """
- category = randomChoice(all_categories) # 随机选一个语言
- line = randomChoice(category_lines[category]) # 随机从这个语言里选一个名字
- category_tensor = torch.tensor([all_categories.index(category)]).long() # 将分类转为Tensor
- line_tensor = lineToTensor(line) # 将名字转为Tensor
- return category, line, category_tensor, line_tensor
- for i in range(10):
- category, line, category_tensor, line_tensor = randomTrainingExample()
- print('category = ', category, '/ line = ', line)
- """
- category = Scottish / line = Aitken
- category = Vietnamese / line = Lieu
- category = Vietnamese / line = Kim
- category = Italian / line = Guidi
- category = German / line = Hoefler
- category = Chinese / line = Pei
- category = Vietnamese / line = an
- category = French / line = De la fontaine
- category = Dutch / line = Tunneson
- category = Polish / line = Nowak
- """
我们之后训练的时候,会使用randomTrainingExample来获取训练的数据, 每次会获取category(种类), line(name的拼写), category_tensor(种类的tensor), line_tensor(name的tensor). 如下面的例子。
我们在定义一个函数用来计算准确率
- def get_accuracy(logit, target):
- corrects = (torch.max(logit, 1)[1].view(target.size()).data == target.data).sum()
- accuracy = 100.0 * corrects/batch_size
- return accuracy.item()
这样,所有都准备好,就可以开始模型的训练了。
模型的训练
关于模型的训练,因为每个name的长度不相同,所以batch_size只能设置为1。所以我在这里,没有每一次计算loss后都进行反向传播和系数优化,而是每print_every次进行反向传播和优化(具体可以看代码中反向传播和优化代码的位置),这样会得到比较好的效果,如果每次都反向传播效果会不稳定。
- criterion = nn.CrossEntropyLoss()
- optimizer = optim.Adam(rnn_name.parameters(),lr=0.005)
- N_EPHOCS = 70
- # 共有20074个数据
- n_iters = 2000 # 训练n_iters个名字
- print_every = 200 # 每print_every轮打印一次结果, 并传递一次误差, 更新系数
- for epoch in range(N_EPHOCS):
- train_running_loss = 0.0
- loss = torch.tensor([0.0]).float()
- train_count = 0
- rnn_name.train()
- # trainging round
- for iter in range(1, n_iters+1):
- # 获取样本
- category, line, category_tensor, line_tensor = randomTrainingExample()
- # 进行训练
- optimizer.zero_grad()
- # reset hidden states
- rnn_name.hidden = rnn_name.init_hidden()
- # forward+backward+optimize
- outputs = rnn_name(line_tensor)
- guess, guess_i = categoryFromOutput(outputs)
- if guess == category:
- train_count = train_count + 1
- loss = loss + criterion(outputs, category_tensor)
- if iter % print_every == 0:
- correct = '✓' if guess == category else '✗ ({})'.format(category)
- print('{} /{} {}'.format(line, guess, correct))
- # 进行反向传播(print_every次传一次误差)
- loss.backward()
- optimizer.step()
- train_running_loss = train_running_loss + loss.detach().item()
- loss = torch.tensor([0.0]).float()
- # 查看准确率
- train_acc = train_count / n_iters * 100
- print('=======')
- print('Epoch : {:0>2d} | Loss : {:<6.4f} | Train Accuracy : {:<6.2f}%'.format(epoch, train_running_loss/n_iters, train_acc))
- print('=======')
最终,在训练集上的准确率可以达到90%以上,我们看一下测试集的准确率。
模型的验证
- test_count = 0
- test_num = 500 # 测试500个名字
- rnn_name.eval()
- for test_iter in range(1, test_num+1):
- category, line, category_tensor, line_tensor = randomTrainingExample()
- outputs = rnn_name(line_tensor)
- guess, guess_i = categoryFromOutput(outputs)
- if guess == category:
- test_count = test_count + 1
- print('Test Accuracy : {:<6.4f}%'.format(test_count/test_num*100))
- # Test Accuracy : 91.8000%
我们随机挑选了500个name进行测试,准确率可以达到90%,这里其实做得不够完美,因为这样训练集和测试集是会有重叠得部分的。
我们单独找一个name,来做一下验证。
- input_data = lineToTensor('WMN')
- out_data = rnn_name(input_data)
- categoryFromOutput(out_data)
- # ('Chinese', 1)
嗯,很迷。这个例子也就大概看一下GRU的实现和RNN在文字处理上的一个应用。我看了一下样本的data,其实data也不是很好。所以,就当熟悉一下如何来书写代码吧。
- 微信公众号
- 关注微信公众号
- QQ群
- 我们的QQ群号
评论