ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【RNN经典案例】使用RNN模型构建人名分类器(RNN实战-姓名分类)

2021-10-12 14:01:10  阅读:276  来源: 互联网

标签:实战 category tensor name 分类器 names hidden size RNN


RNN经典案例-构建人名分类器

前言

在这里插入图片描述

  • 本项目以 RNN 实战流程讲解为主,旨在快速入门上手.
  • 本项目流程规范为作者个人理解,不做指导性建议,读者可根据个人理解梳理.

Step1 - 数据处理

  • 在 names 文件夹中有 18个 .txt 文件,且都是以某种语言名 .txt 命名。 每个 txt 文件中含有很多姓氏名,每个姓氏名独占一行,有些语言使用的是 Unicode 码(含有除了26 英文字母以外的其他字符),我们需要将其统一成 ASCII 码。
  • 将Unicode码转换成标准的 ASCII 码 http://stackoverflow.com/a/518232/2809427
# string.ascii_letters 是大小写各26字母
all_letters = string.ascii_letters + " .,;'"
# 字符的种类数
n_letters = len(all_letters)

# 将Unicode码转换成标准的ASCII码 
def unicode_to_ascii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
        and c in all_letters
    )
print(n_letters) # 字符数为57个
  • 构建语言类别-姓名映射字典,如:{language1: [name1, name2, ...], language2: [name1, name2, ...],}
all_filenames = glob.glob('data/*.txt')
category_names = {}
# 所有类别
all_categories = []
# 读取txt文件,返回 ascii 码的姓名 列表
def readNames(filename):
    names = open(filename).read().strip().split('\n')
    return [unicode_to_ascii(name) for name in names]

for filename in all_filenames:
    category = filename.split('/')[-1].split('.')[0]  # 只获取文件名称,即种类
    all_categories.append(category)
    names = readNames(filename)
    category_names[category] = names
# 语言种类数
n_categories = len(all_categories)
print('n_categories =', n_categories) 
  • 将姓名转化为 Tensors 。为了表征单个的字符, 我们使用独热编码向量 one-hot vector, 该向量的尺寸为 1 * n_letters(每个字符是 2 维向量)。每个由多个字符(每个字符是 2 维)组成的姓名 转化为3维,尺寸为 name_length * 1 * n_letters。
# 将字符转化为 <1 *  n_letters> 的 Tensor
def letter_to_tensor(letter):
    tensor = torch.zeros(1, n_letters)
    letter_index = all_letters.find(letter)
    tensor[0][letter_index] = 1
    return tensor

# 将姓名转化成尺寸为<name_length * 1 * n_letters>的数据
# 使用的是 one-hot 编码方式转化
def name_to_tensor(name):
    tensor = torch.zeros(len(name), 1, n_letters)
    for ni, letter in enumerate(name):
        letter_index = all_letters.find(letter)
        tensor[ni][0][letter_index] = 1
    return tensor

print(letter_to_tensor('J'))
print(name_to_tensor('Jones'))

输出结果:

在这里插入图片描述

Step2 - 定义网络结构

定义网络结构之前我们需要先了解 RNN 的网络结构

在这里插入图片描述
上图中各个参数解释:

  • input: 输入的数据
  • hidden: 神经网络现有的参数矩阵(隐藏层)
  • combined: input 矩阵与 hidden 矩阵合并,两个矩阵的行数一致,input 和 hidden 分别位于新矩阵的左侧和右侧
  • 12h:将输入的数据转化为 hidden 参数的计算过程
  • i2o:对输入的数据转化为 output 的计算过程
  • hidden:当前网络传递给下层网络的参数
  • output:当前网络的输出

结合到我们的项目中,我们可以定义自己的 RNN :

  • input: 字母的向量的特征数量(向量长度)57
  • hidden: 隐藏层特征数量(列数)
  • output_size: 语言数目,18
  • i2h: 隐藏网络参数的计算过程。输入的数据尺寸为 input_size + hidden_size , 输出的尺寸为 hidden_size.
  • i2o: 输出网络参数的计算过程。输入的数据尺寸为 input_size + hidden_size, 输出的尺寸为 output_size.
class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)

    def forward(self, input, hidden):
        #将input和之前的网络中的隐藏层参数合并。
        combined = torch.cat((input, hidden), 1)
        hidden = self.i2h(combined) #计算隐藏层参数
        output = self.i2o(combined) #计算网络输出的结果
        return output, hidden 

    def init_hidden(self):
        #初始化隐藏层参数hidden
        return torch.zeros(1, self.hidden_size)
  • 测试我们定义好的网络
rnn = RNN(input_size=57,  #输入每个字母向量的长度(57个字符)
          hidden_size=128, #隐藏层向量的长度,神经元个数。这里可自行调整参数大小
          output_size=18)  #语言的种类数目

input = letter_to_tensor('A')
hidden = rnn.init_hidden()
output, next_hidden = rnn(input, hidden)
print('output.size =', output.size())
print(output)

运行结果:
在这里插入图片描述
现在我们使用 name_to_tensor 替换 letter_to_tensor 来构造输入的数据。注意在上面的例子中,给 RNN 网络一次输入一个姓名数据,但对该网络而言,是将姓名数据拆分成字母数组数据,逐次输入训练网络,直到这个姓名最后一个字母数组输入完成,才输出真正的预测结果(姓名所属的语言类别)。这里输入 RNN 神经网络的数据的粒度变细,不再是姓名数组数据(三维),而是组成姓名的字母的数组或矩阵(二维)。

  • 准备训练 RNN 在训练前,我们把求所属语言类别的索引值方法封装成函数category_from_output。该函数输入: output ( RNN 网络输出的 output )。该函数输出:语言类别、语言类别索引值。
def category_from_output(output):
    _, top_i = output.data.topk(1) # 获取最大概率的预测
    category_i = top_i[0][0]
    return all_categories[category_i], category_i

category_from_output(output)

Step3 - 定义损失函数

criterion = nn.CrossEntropyLoss()  # 交叉熵损失

Step4 - 定义优化器

learning_rate = 0.005 
# 随机梯度下降优化器
optimizer = torch.optim.SGD(rnn.parameters(),  #给优化器传入rnn网络参数
                            lr=learning_rate) #学习率

Step5 - 模型训练

每轮训练:

  • 创建 input(name_tensor)和 input 对应的语言类别标签(category_tensor)
  • 当输入姓名第一个字母时,需要初始化隐藏层参数。
  • 读取姓名中的每个字母的数组信息,传入 rnn,并将网络输出的 hidden_state 和下一个字母数组信息传入之后的 RNN 网络中.
  • 使用 criterion 比对 最终输出结果 与 姓名真实所属的语言标签 作比较
  • 更新网络参数.

循环往复以上几步

def random_training_pair():   
    # 随机抽取了一种语言                                                                                                            
    category = random.choice(all_categories)
    # 在该语言中抽取一个姓名 
    name = random.choice(category_names[category])
    # 由于pytorch中训练过程中使用的都是tensor结构数据,其中的元素都是浮点型数值,所以这里我们使用LongTensor, 可以保证标签是整数。
    # 另外要注意的是,pytorch中运算的数据都是batch。所以我们要将所属语言的索引值放入一个list中,再将该list传入torch.LongTensor()中
    category_tensor = torch.LongTensor([all_categories.index(category)])
    name_tensor = name_to_tensor(name)
    return category, name, category_tensor, name_tensor


def train(category_tensor, name_tensor):
    rnn.zero_grad() #将rnn网络梯度清零
    hidden = rnn.init_hidden() #只对姓名的第一字母构建起hidden参数

    #对姓名的每一个字母逐次学习规律。每次循环的得到的hidden参数传入下次rnn网络中
    for i in range(name_tensor.size()[0]):
        output, hidden = rnn(name_tensor[i], hidden)

    #比较最终输出结果与 该姓名真实所属语言,计算训练误差
    loss = criterion(output, category_tensor)

    #将比较后的结果反向传播给整个网络
    loss.backward()

    #调整网络参数。有则改之无则加勉
    optimizer.step()

    #返回预测结果  和 训练误差
    return output, loss.item()

Step6 - 验证模型效果

现在我们可以使用一大堆姓名和语言数据来训练 RNN 网络,因为 train 函数会同时返回预测结果和训练误差, 我们可以打印并可视化这些信息。

为了方便,我们每训练 5000 次(5000 个姓名),就打印一个姓名的预测结果,并查看该姓名是否预测正确。我们对每 1000 次的训练累计误差,最终将误差可视化出来。

import time
import math

n_epochs = 100000  # 训练100000次(可重复的从数据集中抽取100000姓名)
print_every = 5000 #每训练5000次,打印一次
plot_every = 1000  #每训练1000次,计算一次训练平均误差

current_loss = 0 #初始误差为0
all_losses = [] #记录平均误差

def time_since(since):
    #计算训练使用的时间
    now = time.time()
    s = now - since
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)

#训练开始时间点
start = time.time()

for epoch in range(1, n_epochs + 1):
    # 随机的获取训练数据name和对应的language
    category, name, category_tensor, name_tensor = random_training_pair()
    output, loss = train(category_tensor, name_tensor)
    current_loss += loss

    #每训练5000次,预测一个姓名,并打印预测情况
    if epoch % print_every == 0:
        guess, guess_i = category_from_output(output)
        correct = '✓' if guess == category else '✗ (%s)' % category
        print('%d %d%% (%s) %.4f %s / %s %s' % (epoch, epoch / n_epochs * 100, time_since(start), loss, name, guess, correct))

    # 每训练5000次,计算一个训练平均误差,方便后面可视化误差曲线图
    if epoch % plot_every == 0:
        all_losses.append(current_loss / plot_every)
        current_loss = 0

绘制训练误差折线图:

import matplotlib.pyplot as plt
%matplotlib inline

plt.figure()
plt.plot(all_losses)

运行结果:

在这里插入图片描述
在这里插入图片描述
从误差图中可以看出,随着训练轮数的增加,模型的每 1000 次训练的平均误差越来越小。

当然我们也可以手动调用模型,查看模型输出结果:

def predict(rnn, input_name, n_predictions=3):
    hidden = rnn.init_hidden()
    #name_tensor.size()[0] 名字的长度(字母的数目)
    for i in range(name_tensor.size()[0]):
        output, hidden = rnn(name_tensor[i], hidden)
    print('\n> %s' % input_name)

    # 得到该姓名预测结果中似然值中前n_predictions大的 似然值和所属语言
    topv, topi = output.data.topk(n_predictions, 1, True)
    for i in range(n_predictions):
        value = topv[0][i]
        category_index = topi[0][i]
        print('(%.2f) %s' % (value, all_categories[category_index]))

predict(rnn, 'Dovesky')
predict(rnn, 'Jackson')
predict(rnn, 'Satoshi')

Step7 - 模型保存

# 模型保存
torch.save(rnn.state_dict(), './model/model_NameClassification.pth')

Step8 - 结果展示

在这里插入图片描述

5000 5% (0m 9s) 2.4735 Rivera / names\Japanese ✗ (names\Spanish)
10000 10% (0m 18s) 1.5551 Jiang / names\Chinese ✓
15000 15% (0m 27s) 2.3822 Eagle / names\Spanish ✗ (names\English)
20000 20% (0m 36s) 2.5122 Nitta / names\Czech ✗ (names\Japanese)
25000 25% (0m 44s) 2.6742 Shalhoub / names\Irish ✗ (names\Arabic)
30000 30% (0m 52s) 2.8427 Lennon / names\Scottish ✗ (names\Irish)
35000 35% (1m 1s) 3.0767 Hino / names\Chinese ✗ (names\Japanese)
40000 40% (1m 9s) 3.2493 Jeong / names\German ✗ (names\Korean)
45000 45% (1m 18s) 0.3656 Affini / names\Italian ✓
50000 50% (1m 26s) 2.1092 Vives / names\Portuguese ✗ (names\Spanish)
55000 55% (1m 34s) 3.7014 Macclelland / names\German ✗ (names\Irish)
60000 60% (1m 43s) 1.8525 Senft / names\German ✓
65000 65% (1m 52s) 0.0594 O'Connell / names\Irish ✓
70000 70% (2m 0s) 0.2395 Dinh / names\Vietnamese ✓
75000 75% (2m 9s) 0.2573 Mcgregor / names\Scottish ✓
80000 80% (2m 17s) 0.3321 Quyen / names\Vietnamese ✓
85000 85% (2m 25s) 3.3420 Diderihs / names\Greek ✗ (names\Russian)
90000 90% (2m 33s) 0.3715 Tieu / names\Vietnamese ✓
95000 95% (2m 41s) 2.3140 Gurin / names\German ✗ (names\French)
100000 100% (2m 49s) 3.7298 Fergus / names\French ✗ (names\Irish)

> Shang
(6.12) names\Chinese
(4.44) names\Vietnamese
(4.00) names\Korean

> Jackson
(5.97) names\Scottish
(4.61) names\English
(3.65) names\Dutch

> ZHU
(4.03) names\Chinese
(4.01) names\Korean
(3.08) names\Vietnamese

Shang & Zhu 均是中国姓氏,可见模型预测的效果还可以!

Step9 - 模型加载

# 加载模型
# 首先实例化模型的类对象
# net = Net()
# 加载训练阶段保存好的模型的状态字典
rnn.load_state_dict(torch.load('./model/model_NameClassification.pth'))

# 利用模型对图片进行预测
# outputs = net(images)

# 共有10个类别, 采用模型计算出的概率最大的作为预测的类别
# _, predicted = torch.max(outputs, 1)

# 打印预测标签的结果
# print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
# 模型测试
predict(rnn, 'Shang')
predict(rnn, 'Wang')
predict(rnn, 'ZHU')
> Shang
(6.12) names\Chinese
(4.44) names\Vietnamese
(4.00) names\Korean

> Wang
(4.72) names\Chinese
(4.68) names\Korean
(2.60) names\Scottish

> ZHU
(4.03) names\Chinese
(4.01) names\Korean
(3.08) names\Vietnamese

Step10 - 完整代码

# 人名分类器
from io import open
import glob
import string
import unicodedata
import random
import time
import math
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# string.ascii_letters 是大小写各26字母
all_letters = string.ascii_letters + " .,;'"
# 字符的种类数
n_letters = len(all_letters)

# 将Unicode码转换成标准的ASCII码
def unicode_to_ascii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
        and c in all_letters
    )


print(n_letters)  # 字符数为57个

all_filenames = glob.glob('./data/names/*.txt')
category_names = {}
# 所有类别
all_categories = []
# 读取txt文件,返回 ascii 码的姓名 列表
def readNames(filename):
    names = open(filename).read().strip().split('\n')
    return [unicode_to_ascii(name) for name in names]


for filename in all_filenames:
    category = filename.split('/')[-1].split('.')[0]
    all_categories.append(category)
    names = readNames(filename)
    category_names[category] = names
# 语言种类数
n_categories = len(all_categories)
print('n_categories =', n_categories)

# 将字符转化为 <1 *  n_letters> 的 Tensor
def letter_to_tensor(letter):
    tensor = torch.zeros(1, n_letters)
    letter_index = all_letters.find(letter)
    tensor[0][letter_index] = 1
    return tensor

# 将姓名转化成尺寸为<name_length * 1 * n_letters>的数据
# 使用的是 one-hot 编码方式转化
def name_to_tensor(name):
    tensor = torch.zeros(len(name), 1, n_letters)
    for ni, letter in enumerate(name):
        letter_index = all_letters.find(letter)
        tensor[ni][0][letter_index] = 1
    return tensor


print(letter_to_tensor('J'))
print(name_to_tensor('Jones'))


class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
        self.i2o = nn.Linear(input_size + hidden_size, output_size)

    def forward(self, input, hidden):
        # 将input和之前的网络中的隐藏层参数合并。
        combined = torch.cat((input, hidden), 1)
        hidden = self.i2h(combined) # 计算隐藏层参数
        output = self.i2o(combined) # 计算网络输出的结果
        return output, hidden

    def init_hidden(self):
        # 初始化隐藏层参数hidden
        return torch.zeros(1, self.hidden_size)


# 测试模型
# input = letter_to_tensor('A')
# hidden = rnn.init_hidden()
# output, next_hidden = rnn(input, hidden)
# print('output.size =', output.size())
# print(output)


def category_from_output(output):
    _, top_i = output.data.topk(1)
    category_i = top_i[0][0]
    return all_categories[category_i], category_i


def random_training_pair():
    # 随机抽取了一种语言
    category = random.choice(all_categories)
    # 在该语言中抽取一个姓名
    name = random.choice(category_names[category])
    # 由于pytorch中训练过程中使用的都是tensor结构数据,其中的元素都是浮点型数值,所以这里我们使用LongTensor, 可以保证标签是整数。
    # 另外要注意的是,pytorch中运算的数据都是batch。所以我们要将所属语言的索引值放入一个list中,再将该list传入torch.LongTensor()中
    category_tensor = torch.LongTensor([all_categories.index(category)])
    name_tensor = name_to_tensor(name)
    return category, name, category_tensor, name_tensor


def train(category_tensor, name_tensor):
    rnn.zero_grad()  # 将rnn网络梯度清零
    hidden = rnn.init_hidden()  # 只对姓名的第一字母构建起hidden参数

    # 对姓名的每一个字母逐次学习规律。每次循环的得到的hidden参数传入下次rnn网络中
    for i in range(name_tensor.size()[0]):
        output, hidden = rnn(name_tensor[i], hidden)

    # 比较最终输出结果与 该姓名真实所属语言,计算训练误差
    loss = criterion(output, category_tensor)

    # 将比较后的结果反向传播给整个网络
    loss.backward()

    # 调整网络参数。有则改之无则加勉
    optimizer.step()

    # 返回预测结果  和 训练误差
    return output, loss.item()


def time_since(since):
    # 计算训练使用的时间
    now = time.time()
    s = now - since
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)


def predict(rnn, input_name, n_predictions=3):
    """模型预测"""
    hidden = rnn.init_hidden()
    # name_tensor.size()[0] 名字的长度(字母的数目)
    name_tensor = name_to_tensor(input_name)
    for i in range(name_tensor.size()[0]):
        output, hidden = rnn(name_tensor[i], hidden)
    print('\n> %s' % input_name)

    # 得到该姓名预测结果中似然值中前n_predictions大的 似然值和所属语言
    topv, topi = output.data.topk(n_predictions, 1, True)
    for i in range(n_predictions):
        value = topv[0][i]
        category_index = topi[0][i]
        print('(%.2f) %s' % (value, all_categories[category_index]))


if __name__ == '__main__':
    # 初始化模型
    rnn = RNN(input_size=57,  # 输入每个字母向量的长度(57个字符)
              hidden_size=128,  # 隐藏层向量的长度,神经元个数。这里可自行调整参数大小
              output_size=18)  # 语言的种类数目
    # 损失函数
    criterion = nn.CrossEntropyLoss()
    # 定义优化器
    learning_rate = 0.005
    optimizer = torch.optim.SGD(rnn.parameters(),  # 给优化器传入rnn网络参数
                                lr=learning_rate)  # 学习率

    # 模型训练
    n_epochs = 100000  # 训练100000次(可重复的从数据集中抽取100000姓名)
    print_every = 5000  # 每训练5000次,打印一次
    plot_every = 1000  # 每训练1000次,计算一次训练平均误差

    current_loss = 0  # 初始误差为0
    all_losses = []  # 记录平均误差

    # 训练开始时间点
    start = time.time()

    for epoch in range(1, n_epochs + 1):
        # 随机的获取训练数据name和对应的language
        category, name, category_tensor, name_tensor = random_training_pair()
        output, loss = train(category_tensor, name_tensor)
        current_loss += loss

        # 每训练5000次,预测一个姓名,并打印预测情况
        if epoch % print_every == 0:
            guess, guess_i = category_from_output(output)
            correct = 'True' if guess == category else 'False (%s)' % category
            print('%d %d%% (%s) %.4f %s / %s %s' % (
            epoch, epoch / n_epochs * 100, time_since(start), loss, name, guess, correct))

        # 每训练5000次,计算一个训练平均误差,方便后面可视化误差曲线图
        if epoch % plot_every == 0:
            all_losses.append(current_loss / plot_every)
            current_loss = 0

    # 损失绘图
    plt.figure()
    plt.plot(all_losses)
    plt.show()

    # 预测模型
    predict(rnn, 'Shang')
    predict(rnn, 'Jackson')
    predict(rnn, 'ZHU')

    # 模型保存
    torch.save(rnn.state_dict(), './model/model_NameClassification.pth')
    
    # 加载模型与测试
	# 首先实例化模型的类对象
	# net = Net()
	# 加载训练阶段保存好的模型的状态字典
	rnn.load_state_dict(torch.load('./model/model_NameClassification.pth'))
	
	# 利用模型对图片进行预测
	# outputs = net(images)
	
	# 共有10个类别, 采用模型计算出的概率最大的作为预测的类别
	# _, predicted = torch.max(outputs, 1)
	
	# 打印预测标签的结果
	# print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
	# 模型测试
	predict(rnn, 'Shang')
	predict(rnn, 'Wang')
	predict(rnn, 'ZHU')
	

参考Link


加油!

感谢!

努力!

标签:实战,category,tensor,name,分类器,names,hidden,size,RNN
来源: https://blog.csdn.net/qq_46092061/article/details/120721136

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有