NLP自然语言处理-经典RNN案例-人名分类器

二、实践:

1、代码:
from io import open
import glob
import os
import string
import unicodedata
import random
import time
import math
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)


# print("n_letters:", n_letters)
# n_letters: 57

# 1、函数的作用是去掉一些语言中的重音标记
def unicodeToAscii(s):
    return ''.join(c for c in unicodedata.normalize('NFD', s)
                   if unicodedata.category(c) != 'Mn'
                   and c in all_letters)


# 调用
s = "8▔Lucy"
a = unicodeToAscii(s)
# print(a)
# Lucy

data_path = "/Users/weixiujuan/Downloads/data/names/"


# 2、读取文件转换成列表
def readLines(filename):
    # 打开指定的文件并读取所有的内容,使用strip()去除掉两侧的空白符,然后以'\n'为换行符进行切分
    lines = open(filename, encoding='utf-8').read().strip().split('\n')
    return [unicodeToAscii(line) for line in lines]


filename = data_path + "Chinese.txt"
result = readLines(filename)
# print(result[:20])
# ['Ang', 'AuYong', 'Bai', 'Ban', 'Bao', 'Bei', 'Bian', 'Bui', 'Cai', 'Cao', 'Cen', 'Chai', 'Chaim', 'Chan', 'Chang', 'Chao', 'Che', 'Chen', 'Cheng', 'Cheung']


# 构建的category_lines形如:{"English":["Lily","Susan","Kobe"],"Chinese":["Zhang San]}
category_lines = {}
# all_categoryies形如:["English",...,"Chinese"]
all_categories = []

# 3、读取指定路径下的txt文件,使用glob,path中可以使用正则表达式
for filename in glob.glob(data_path + '*.txt'):
    # 获取每个文件的文件名,就是对应的名字类别
    category = os.path.splitext(os.path.basename(filename))[0]
    # 将其逐一装到all_categories列表中
    all_categories.append(category)
    # 然后读取每个文件的内容,形成名字列表
    lines = readLines(filename)
    # 按照对应的类别,将名字列表写入到category_lines字典中
    category_lines[category] = lines

# 查看类别总数
n_categories = len(all_categories)


# print("n_categories:", n_categories)
# n_categories: 18
# 随便查看其中的一些内容
# print(category_lines['Italian'][:5])

# ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']

# 4、将任命转化为对应onehot张量表示:
# 将字符串(单词粒度)转化为张量表示,如:"ab" -->
# tensor([[1.,0.,0.,...,0.]],
#         [[0.,1.,0.,...,0.]])
def lineToTensor(line):
    """将任命转化为对应onehot张量表示,参数line是输入的人名"""
    # 首先初始化一个全0张量,它的形状(len(line), 1, n_letters)
    # 代表任命中的每个字母用一个 1 * n_letters的张量表示。
    tensor = torch.zeros(len(line), 1, n_letters)
    # 遍历这个人名中的每个字符索引和字符,并搜索其对应的索引,将该索引位置置为1
    for li, letter in enumerate(line):
        # 使用字符串方法find找到每个字符在all_letters中的索引
        # 它也是我们生成onehot张量中1的索引位置
        tensor[li][0][all_letters.find(letter)] = 1

        # 返回结果
        return tensor


# 调用:
# line = "Bai"
# line_tensor = lineToTensor(line)
# print("line_tensor:", line_tensor)


# x = torch.tensor(([1, 2, 3, 4]))
# print(x.shape)
# y = torch.unsqueeze(x, 0)
# print(y)
# print(y.shape)
# z = torch.unsqueeze(x, 1)
# print(z)
# print(z.shape)

# 5、构建RNN模型
# 5.1、使用nn.RNN构建完成传统RNN使用类

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        """初始化函数中有4个参数,分别代表RNN输入最后一维尺寸,RNN的隐层最后一维尺寸,RNN层数"""
        # input_size:代表RNN输入的最后一个维度。
        # hidden_size:代表RNN隐藏层的最后一个维度。
        # output_size:代表RNN网络最后线性层的输出维度。
        # num_layers:代表RNN网络的层数
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers

        # 实例化预定义的nn.RNN,它的三个参数分别是inpiut_size,hidden_size,num_layer
        self.rnn = nn.RNN(input_size, hidden_size, num_layers)
        # 实例化全连接线性层,nn.Linear, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.linear = nn.Linear(hidden_size, output_size)
        # 实例化nn中预定的Softmax层,用于从输出层获得类别结果
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input1, hidden):
        """
        完成传统RNN中的主要逻辑。
        输入参数:
        input:代表人名分类器中的输入张量,它的形状是1 * n_letters
        hidden:代表RNN的隐层张量,它的形状是self.num_layers * 1 * self.hidden_size
        """
        # 因为预定义的nn.RNN要求输入维度一定是三维张量,因此在这里使用unsqueeze(0)扩展一个维度
        input1 = input1.unsqueeze(0)
        # 将input1和hidden输入到传统RNN的实例化对象中,如果num_layers=1,rr恒等于hn
        rr, hn = self.rnn(input1, hidden)
        # 将从RNN中获得的结果通过线性变换和softmax层处理,同时返回hn作为后续RNN的输入
        return self.softmax(self.linear(rr)), hn

    def initHidden(self):
        """初始化隐层张量"""
        # 初始化一个(self.num_layers,1,self.hidden_size)形状的全0隐藏层张量,维度是3
        return torch.zeros(self.num_layers, 1, self.hidden_size)


# 5.2、构建LSTM模型:
class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size, out_size, num_layers=1):
        """初始化函数的参数与传统RNN相同"""
        # input_size:代表输入张量x中最后一个维度
        # hidden_size:代表隐藏层张量的最后一个维度
        # out_size:代表线性层最后的输出维度
        # num_layers:代表LSTM网络的层数
        super(LSTM, self).__init__()
        # 将hidden_size与num_layers等传入其中
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.out_size = out_size
        self.num_layers = num_layers

        # 实例化预定义的nn.LSTM
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers)
        # 实例化nn.Linear,这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.linear = nn.Linear(hidden_size, out_size)
        # 实例化nn中预定的Softmax层,用于从输出层获得类别结果
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden, c):
        """在主要逻辑函数中多出一个参数c,也就是LSTM中的细胞状态张量"""
        # 使用unsqueeze(0)扩展一个维度
        input = input.unsqueeze(0)
        # 将input,hidden以及初始化的c传入lstm中
        rr, (hn, c) = self.lstm(input, (hidden, c))
        # 最后返回处理后的rr, hn, c
        return self.softmax(self.linear(rr)), hn, c

    def initHiddenAndC(self):
        """初始化函数不仅初始化hidden还要初始化细胞状态c,它们形状相同"""
        c = hidden = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden, c


# 5.3、构建GRU模型:
# 使用nn.GRU构建完成传统RNN使用类
# GRU与传统RNN的外部形式相同,都是只传递隐层张量,因此只需要更改预定义层的名字

class GRU(nn.Module):
    def __init__(self, input_size, hidden_size, output_szie, num_layers=1):
        # 它的三个参数分别是
        # input_size:代表输入张量x最后一个维度
        # hidden_size:代表隐藏层最后一个维度
        # output_szie:代表指定的线性层输出的维度
        # num_layers:代表GRU网络的层数
        super(GRU, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_szie = output_szie
        self.num_layers = num_layers

        # 实例化预定义的nn.GRU,
        self.gru = nn.GRU(input_size, hidden_size, num_layers)
        # 实例化线性层的对象
        self.linear = nn.Linear(hidden_size, output_szie)
        # 定义softmax对象,作用是从输出张量中得到类别分类
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden):
        input = input.unsqueeze(0)
        rr, hn = self.gru(input, hidden)
        return self.softmax(self.linear(rr)), hn

    def initHidden(self):
        return torch.zeros(self.num_layers, 1, self.hidden_size)


# 6、实例化参数:
# 因为是onehot编码,输入张量最后一组的尺寸就是n_letters
input_size = n_letters

# 定义隐藏层的最后一维尺寸大小
n_hidden = 128

# 输出尺寸为语言类别总数n_categories
output_size = n_categories

# num_layer使用默认值,num_layers = 1

# 7、输入参数:
# 假如我们以一个字母B作为RNN的首次输入,它通过lineToTensor转为张量
# 因为我们的lineToTensor输出是三维张量,而RNN类需要的是二维张量
# 因此需要使用squeeze(0)降低一个维度
input = lineToTensor('B').squeeze(0)

# 初始化一个三维的隐层0张量,也是初始的细胞状态张量
hidden = c = torch.zeros(1, 1, n_hidden)

# 8、调用
rnn = RNN(n_letters, n_hidden, output_size)
lstm = LSTM(n_letters, n_hidden, output_size)
gru = GRU(n_letters, n_hidden, output_size)

rnn_output, next_hidden = rnn(input, hidden)
print("rnn:", rnn_output)
print("rnn_shape:", rnn_output.shape)
print("***********")

lstm_output, next_hidden1, c = lstm(input, hidden, c)
print("lstm:", lstm_output)
print("lstm_shape:", lstm_output.shape)
print("***********")

gru_output, next_hidden2 = gru(input, hidden)
print("gru:", gru_output)
print("gru_shape:", gru_output.shape)


# 第四步:构建训练函数并进行训练
# 4.1、从输出结果中获得指定类别函数:
def categoryFromOutput(output):
    """
    从输出结果中获得指定类别,参数为输出张量
    output:从输出结果中得到指定的类别
    """
    # 从输出张量中返回最大的值和索引对象,我们这里主要需要这个索引
    top_n, top_i = output.topk(1)
    # 从top_i对象中取出索引的值
    category_i = top_i[0].item()
    # 根据索引值获得对应语言类别,返回语言类别和索引值
    return all_categories[category_i], category_i


# x = torch.arange(1, 6)
# print(x)
# res = torch.topk(x, 3)
# print(res)

#  输入参数:
# 将上一步中gru的输出作为函数的输入
output = gru_output

# 调用
category, category_i = categoryFromOutput(output)
print("category:", category)
print("category_i:", category_i)


# category: Italian
# category_i: 12

# 4.2、随机生成训练数据
def randomTrainingExample():
    """该函数用于随机产生训练数据"""
    # 首先使用random的choice方法从all_categories随机选择一个类别
    category = random.choice(all_categories)
    # 然后在通过category_lines字典去category类别对应的名字列表
    # 之后再从列表中随机取一个名字
    line = random.choice(category_lines[category])
    # 接着讲这个类别在所有类别列表中的索引封装成tensor,得到类别张量category_tensor
    category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long)
    # 最后,将随机取到的名字通过函数lineToTensor转化为onehot张量表示
    line_tensor = lineToTensor(line)
    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_tensor =', category_tensor)
print('line_tensor = ', line_tensor)

# 构建传统RNN训练函数:
# 定义损失函数为nn.NLLLoss,因为RNN的最后一层是nn.LogSoftmax,两者的内部计算逻辑正好能够吻合。
criterion = nn.NLLLoss()

# 设置学习率为0.05
learning_rate = 0.05


def trainRNN(category_tensor, line_tensor):
    """定义训练函数,
       它的两个参数是:
       category_tensor:类别的张量表示,相当于训练数据的标签,
        line_tensor:名字的张量表示,相当于对应训练数据特征
    """

    # 在函数中,首先通过实例化对象rnn初始化隐层张量
    hidden = rnn.initHidden()

    # 关键一步:将模型结构中的梯度归0
    rnn.zero_grad()

    # 下面开始进行循环遍历训练,将训练数据line_tensor的每个字符住个传入rnn之中,并迭代更新hidden,得到最终结果
    for i in range(line_tensor.size()[0]):
        output, hidden = rnn(line_tensor[i], hidden)

    # 因为我们的rnn对象由nn.RNN实例化得到,最终输出形状是三维张量,为了满足于category_tensor
    # 进行对比计算损失,需要减少第一个维度,这里使用squeeze()方法
    loss = criterion(output.squeeze(0), category_tensor)

    # 损失进行反向传播
    loss.backward()

    # 显示的更新模型中所有参数
    for p in rnn.parameters():
        # 将参数的张量表示与参数的梯度乘以学习率的结果相加以此来更新参数,并进行覆盖更新
        p.data.add_(-learning_rate, p.grad.data)

    # 返回RNN最终的输出结果output和损失的值loss
    return output, loss.item()


# 构建LSTM训练函数
# 与传统RNN相比多出细胞状态c

def trainLSTM(category_tensor, line_tensor):
    hidden, c = lstm.initHiddenAndC()
    lstm.zero_grad()
    for i in range(line_tensor.size()[0]):
        # 返回output,hidden以及细胞状态c:
        output, hidden, c = lstm(line_tensor[i], hidden, c)
    # 将预测张量,和目标标签张量输入损失函数中
    loss = criterion(output.squeeze(0), category_tensor)
    loss.backward()
    # 进行参数的显示更新
    for p in lstm.parameters():
        p.data.add_(-learning_rate, p.grad.data)
    return output, loss.item()


# 构建GRU训练函数:
# 与传统RNN完全相同,只不过名字改成了GRU
def trainGRU(category_tensor, line_tensor):
    # 注意GRU网络初始化的时候只需要初始化一个隐藏层的张量
    hidden = gru.initHidden()
    # 关键一步:将模型结构中的梯度归0
    gru.zero_grad()
    for i in range(line_tensor.size()[0]):
        output, hidden = gru(line_tensor[i], hidden)
    loss = criterion(output.squeeze(0), category_tensor)
    loss.backward()

    for p in gru.parameters():
        p.data.add_(-learning_rate, p.grad.data)
    return output, loss.item()


# 构建时间计算函数:
def timeSince(since):
    """获得每次打印的训练耗时,since是训练开始时间"""
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟,并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)


# 输入参数:
# 假定模型训练开始时间是10min之前
since = time.time() - 10 * 60

# 调用:
period = timeSince(since)
print(period)
# 10m 0s

# 构建训练过程的日志打印函数:
# 设置训练迭代次数
n_iters = 100000
# 设置结果的打印间隔
print_every = 50
# 设置绘制损失曲线上的制图间隔
plot_every = 10


def train(train_type_fn):
    """训练过程的日志打印函数,参数train_tyoe_fn代表选择哪种模型训练函数,如:trainRNN"""
    # 每个制图间隔损失保存列表
    all_losses = []
    # 获得训练开始时间戳
    start = time.time()
    # 设置初始间隔损失为0
    current_loss = 0
    # 从1开始进行训练迭代,共n_iters次
    for iter in range(1, n_iters + 1):
        # 通过randomTrainingExample函数随机获取一组训练数据和对应的类别
        category, line, category_tensor, line_tensor = randomTrainingExample()
        # 将训练数据和对应类别的张量表示传入到train函数中
        output, loss = train_type_fn(category_tensor, line_tensor)
        # 计算制图间隔中的总损失
        current_loss += loss
        # 如果迭代数能够整除打印间隔
        if iter % print_every == 0:
            # 取代迭代步上的output通过categoryFromOutput函数获得对 应的类别和类别索引
            guess, guess_i = categoryFromOutput(output)
            # 然后和真实的类别category做比较,如果相同则打对号,否则打叉号
            correct = 'True' if guess == category else 'False(%s)' % category
            # 打印迭代步,迭代步百分比,当前训练耗时,损失,该步预测的名字,以及是否正确
            print('%d %d%% (%s) %.4f %s / %s %s ' % (iter, iter / n_iters * 100, timeSince(start), loss, line, guess,
                                                     correct))

        # 如果迭代数能够整除制图间隔
        if iter % plot_every == 0:
            # 将保存该间隔中的平均损失到all_losses列表中
            all_losses.append(current_loss / plot_every)
            # 间隔损失重置为0
            current_loss = 0

    # 返回对应的总损失列表和训练耗时
    return all_losses, int(time.time() - start)


# 开始训练传统RNN、LSTM、GRU模型并制作对比图
# 调用train函数,分别进行RNN、LSTM、GRU模型训练
# 并返回各自的全部损失,以及训练耗时用于制图
all_losses1, period1 = train(trainRNN)
all_losses2, period2 = train(trainLSTM)
all_losses3, period3 = train(trainGRU)

# 绘制损失对比曲线,训练耗时对比柱状图
# 创建画布0
plt.figure(0)
# 绘制损失对比曲线
plt.plot(all_losses1, label="RNN")
plt.plot(all_losses2, color="red", label="LSTM")
plt.plot(all_losses3, color="orange", label="GRU")
plt.legend(loc='upper left')

# 创建画布1
plt.figure(1)
x_data = ["RNN", "LSTM", "GRU"]
y_data = [period1, period2, period3]
# 绘制训练耗时对比柱状图
# plt.bar(range(len(x_data), y_data, tick_label=x_data))
res = plt.bar(range(len(x_data)), y_data, tick_label=x_data)
print(res)
plt.show()

2、运行结果:
image.png
image.png
image.png
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容