PyTorch深度学习实践概论笔记13-循环神经网络高级篇-分类

72 篇文章 28 订阅
28 篇文章 17 订阅

PyTorch深度学习实践概论笔记12-循环神经网络基础篇中简单介绍了RNN,接下来13讲,我们介绍一个关于神经网络的应用:实现一个循环神经网络的分类器。

1 RNN Classifier – Name Classification

用RNN做一个分类器。

先看看这个问题。现在有一个数据集,数据集里有人名和对应的国家,我们需要训练一个模型,输入一个新的名字,模型能预测出这个名字是基于哪种语言的(18种不同的语言,18分类)。

2 Revision

回顾上一讲。 

在自然语言处理中,通常的方式:①先把词或字变成一个one-hot向量,one-hot向量维度高,而且过于稀疏,所以一般来说先通过嵌入层(Embed)把one-hot向量转化成低维的稠密向量,②然后经过RNN,隐层的输出不一定和最终要求的目标一致,所以要用一个线性层把输出映射成和我们的要求一致。

3 Our Model

我们的需求是输出名字所属的语言分类,我们对O1-O5这些输出是没有要求的,即不需要对所有的隐层输出做线性变换,为了解决这个问题,我们可以把网络简化,如下图所示。

输入向量经过嵌入层之后,输入到RNN,输出最终的隐层状态,最终的隐层状态经过一个线性层,我们分成18个类别,就可以实现名字分类的任务了。

这一讲使用的模型如下:

输入首先经过嵌入层,然后使用GRU模型,然后使用线性层,注意输入的是最后一个隐藏状态的输出,经过变换变成一个18维的输出。

看一下数据,只有两列:Name和Country。注意输入的每一个名字都是一个序列(x1,x2,...xN),而且序列的长短不一样。

4 Implementation

首先看一下主要的循环是怎么写的。 

4.1 Implementation – Main Cycle

代码如下:

if __name__ == '__main__':
    #N_CHARS:字符数量(输入的是英文字母,每一个字符都要转变成one-hot向量,这是自己设置的字母表的大小)
    #HIDDEN_SIZE:隐层数量(GRU输出的隐层的维度)
    #N_COUNTRY:一共有多少个分类
    #N_LAYER:设置用几层的GRU
    #实例化分类模型

    classifier = RNNClassifier(N_CHARS, HIDDEN_SIZE, N_COUNTRY, N_LAYER)

    #判断是否使用GPU训练模型
    if USE_GPU:

    	device = torch.device("cuda:0")

    	classifier.to(device)
     
    #构造损失函数和优化器  
    criterion = torch.nn.CrossEntropyLoss()

    optimizer = torch.optim.Adam(classifier.parameters(), lr=0.001)


    start = time.time()  #计算一下时间
    print("Training for %d epochs..." % N_EPOCHS)
    acc_list = []
    #每一次epoch做一次训练和测试
    for epoch in range(1, N_EPOCHS + 1):
    	# Train cycle
    	trainModel()
    	acc = testModel()
     	#测试结果添加到acc_list列表,可以绘图等
    	acc_list.append(acc)

计算运行时间的函数time_since()的代码如下:

def time_since(since):
    s = time.time() - since
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)

记录测试的准确率,代码如下:

import matplotlib.pyplot as plt
import numpy as np


epoch = np.arange(1, len(acc_list) + 1, 1)

acc_list = np.array(acc_list)

plt.plot(epoch, acc_list)

plt.xlabel('Epoch')

plt.ylabel('Accuracy')

plt.grid()

plt.show()

回忆训练的基本过程(四步):

准备数据–定义模型–构造损失函数和优化器–训练过程

4.2 Implementation – Preparing Data

首先看一下数据上的准备。 

4.2.1 Name的处理

①拿到的是字符串,先转变成序列,转成字符列表,列表里面的每一个数就是名字里面的每一个字符。

②接下来做词典,可以用ASCII表,ASCII表是128个字符,我们把字典长度设置成128,求每一个字符对应的ASCII值,拼成我们想要的序列。上图中的最右表中每一个数并不是一个数字,而是一个one-hot向量。例如77,就是一个128维的向量,第77个数的值为1,其他的值都是0。对于Embedding(嵌入层)来说,只要告诉嵌入层第几个维度是1就行了,所以只需要把ASCII值放在这就行了。  

序列长短不一应该怎么解决

③如上图左,每一行是一个序列,我们解决序列长短不一的方法是padding(因为张量必须保证所有的数据都添满,不然就不是张量),如上图右侧,在做一个batch的时候,我们看这一个batch里面哪一个字符串的长度最长,然后把其他字符串填充成和它一样的长度,这样就能保证可以构成一个张量,因为每个维度的数量不一样是没办法构成张量的

4.2.2 Country的处理

我们需要把各个分类(国家)转成一个分类索引(index)分类索引必须是0开始的整数,不能直接用字符串作为我们的分类标签。 

整个数据集一共18个国家,做成一个词典。

代码如下:

import gzip
import csv

class NameDataset(Dataset):

    def __init__(self, is_train_set=True):
    	#从gz当中读取数据
    	filename = 'data/names_train.csv.gz' if is_train_set else 'data/names_test.csv.gz'

    	with gzip.open(filename, 'rt') as f:
    		reader = csv.reader(f) #每一行都是(name,country)的元组
    		rows = list(reader)
      	#将names和countries保存在list中
    	self.names = [row[0] for row in rows]
    	self.len = len(self.names)
    	self.countries = [row[1] for row in rows]
     	#将countries和它的index保存在list和dictionary中
    	self.country_list = list(sorted(set(self.countries)))
    	self.country_dict = self.getCountryDict()
    	self.country_num = len(self.country_list)#国家的个数

    #提供索引访问
    def __getitem__(self, index):
    	return self.names[index], self.country_dict[self.countries[index]]

   #返回dataset的长度 
    def __len__(self):
    	return self.len  
    
    #将list转化成dictionary 
    def getCountryDict(self):
    	country_dict = dict()
    	for idx, country_name in enumerate(self.country_list, 0):
    		country_dict[country_name] = idx
    	return country_dict

    
    #给定index返回country,方便展示
    def idx2country(self, index):
    	return self.country_list[index]

    
    #返回country的数目
    def getCountriesNum(self):
    	return self.country_num
     
            
#Prepare Dataset and DataLoader
# Parameters
HIDDEN_SIZE = 100
BATCH_SIZE = 256
N_LAYER = 2
N_EPOCHS = 100
N_CHARS = 128
USE_GPU = False
#训练数据                   
trainset = NameDataset(is_train_set=True)
trainloader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True)
#测试数据
testset = NameDataset(is_train_set=False)
testloader = DataLoader(testset, batch_size=BATCH_SIZE, shuffle=False)
#N_COUNTRY is the output size of our model
N_COUNTRY = trainset.getCountriesNum()

注意上述代码读取数据集为什么不用Numpy?因为读取数据集有很多种方式,如果是pickle/HDFS/HD5类型的数据,要就要用相应的包。

根据人名找到他的国家对应的index:

4.3 Implementation – Model Design

4.3.1 Implementation – Model Design

先看看和GRU相关的参数:hidden_size和n_layers。 

 注意Embedding层的输入、输出维度:

 还有GRU的输入、输出维度:

 这里n_directions设置单向还是双向的:

代码如下:

class RNNClassifier(torch.nn.Module):
    def __init__(self, input_size, hidden_size, output_size, n_layers=1, bidirectional=True):
    	super(RNNClassifier, self).__init__()
     	#parameters of GRU layer
    	self.hidden_size = hidden_size
    	self.n_layers = n_layers
     	#What is the Bi-Direction RNN/LSTM/GRU?
    	self.n_directions = 2 if bidirectional else 1
    	
     	#The input of Embedding Layer with shape:𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒
        #The output of Embedding Layer with shape:𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒
     	self.embedding = torch.nn.Embedding(input_size, hidden_size)
    	#The inputs of GRU Layer with shape:
    	#𝑖𝑛𝑝𝑢𝑡: 𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒
    	#ℎ𝑖𝑑𝑑𝑒𝑛: 𝑛𝐿𝑎𝑦𝑒𝑟𝑠 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒
    	#The outputs of GRU Layer with shape:
    	#𝑜𝑢𝑡𝑝𝑢𝑡: 𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠
    	#ℎ𝑖𝑑𝑑𝑒𝑛: 𝑛𝐿𝑎𝑦𝑒𝑟𝑠 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒
     	self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers,bidirectional=bidirectional)
    	self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size)


    def _init_hidden(self, batch_size):
    	hidden = torch.zeros(self.n_layers * self.n_directions,batch_size, self.hidden_size)
    	return create_tensor(hidden)

下面具体看看什么是双向的神经网络吧。

4.3.2 Implementation – Bi-direction RNN/LSTM/GRU

序列的forward方向流程图(注意forward不是正向传播):

上图的情况x_(N-1)只包含过去的信息,但是有时候在NLP中也需要考虑未来的信息。

序列的backward方向流程图(注意backward不是反向传播):

接着

 最后

这样的神经网络称双向的神经网络,backward最后得到h_N^b,输出是上面的h0、h1...hN。

最后输出的hidden只有两个,公式如下:

4.3.3 Implementation – Model Design

再接着看,这个地方就需要乘上n_directions:

然后再看一下forward过程

首先我们做的是矩阵转置:input = input.t()。接着保存batch_size的值,之后用来构造h0。

然后嵌入层:embedding = self.embedding(input),维度就变成下图的维度。

注意看下图,如果之后的都padding为0之后没有必要参与运算,pytorch提供了下面的功能来加快运算。 

利用这行代码gru_input = pack_padded_sequence(embedding, seq_lengths),输入输出如下:

源码:

如果还是不太清楚,接着往下看:

直接把左侧非0的列排到右侧,把填充的0去掉,GRU可以处理长短不一的数据序列(数据长度保存),但是不能使用打包函数,想要打包的话,必须按照长度降序排列。降序排列如下图:

排好之后,重新计算:

这样做之后工作效率更高了。

整体代码如下:

class RNNClassifier(torch.nn.Module):

    def __init__(self, input_size, hidden_size, output_size, n_layers=1, bidirectional=True):

    	super(RNNClassifier, self).__init__()

    	self.hidden_size = hidden_size

    	self.n_layers = n_layers

    	self.n_directions = 2 if bidirectional else 1

    	
     	self.embedding = torch.nn.Embedding(input_size, hidden_size)

    	#The inputs of GRU Layer with shape:

    	#𝑖𝑛𝑝𝑢𝑡: 𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒

    	#ℎ𝑖𝑑𝑑𝑒𝑛: 𝑛𝐿𝑎𝑦𝑒𝑟𝑠 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒

    	#The outputs of GRU Layer with shape:

    	#𝑜𝑢𝑡𝑝𝑢𝑡: 𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠

    	#ℎ𝑖𝑑𝑑𝑒𝑛: 𝑛𝐿𝑎𝑦𝑒𝑟𝑠 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒
     	self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers,bidirectional=bidirectional)


    	self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size)


    def _init_hidden(self, batch_size):

    	hidden = torch.zeros(self.n_layers * self.n_directions,batch_size, self.hidden_size)

    	return create_tensor(hidden)
         
    def forward(self, input, seq_lengths):

    	# input shape : B x S -> S x B

    	input = input.t()
     	#Save batch-size for make initial hidden

    	batch_size = input.size(1)

    	
     	#Initial hidden with shape:

    	#(𝑛𝐿𝑎𝑦𝑒𝑟 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛𝑠, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒)
     	hidden = self._init_hidden(batch_size)
      	#Result of embedding with shape:

    	#(𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒)

    	embedding = self.embedding(input)

    	
     	# pack them up
      	#The first parameter with shape:

      	#(𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒)

      	#The second parameter is a tensor, which is a list of sequence length of each batch element.

    	#Result of embedding with shape:(𝑠𝑒𝑞𝐿𝑒𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒)

    	#It returns a PackedSquence object.
     	gru_input = pack_padded_sequence(embedding, seq_lengths)

    	
     	#The output is a PackedSequence object, actually it is a tuple.

     	#the shape of hidden, which we concerned, with shape:

     	#(𝑛𝐿𝑎𝑦𝑒𝑟𝑠 ∗ 𝑛𝐷𝑖𝑟𝑒𝑐𝑡𝑖𝑜𝑛, 𝑏𝑎𝑡𝑐ℎ𝑆𝑖𝑧𝑒, ℎ𝑖𝑑𝑑𝑒𝑛𝑆𝑖𝑧𝑒)
     	output, hidden = self.gru(gru_input, hidden)

    	if self.n_directions == 2:

    		hidden_cat = torch.cat([hidden[-1], hidden[-2]], dim=1)

    	else:

    		hidden_cat = hidden[-1]

    	fc_output = self.fc(hidden_cat)

    	return fc_output

以上内容是模型相应的设定。

4.4 Implementation – Convert name to tensor

接下来看看name转化成tensor的过程。 

 转化过程如下:

①name转换成一个一个字符,转化成对应的ASCII值。

②填充:

③转置:

④降序排列:

看看make_tensors函数。

把每个字符变成列表。代码如下:

4.5 Implementation – One Epoch Training

训练:

代码如下: 

def create_tensor(tensor):
    if USE_GPU:
        device = torch.device("cuda:0")
        tensor = tensor.to(device)
    return tensor

def trainModel():
    total_loss = 0
    for i, (names, countries) in enumerate(trainloader, 1):
        inputs, seq_lengths, target = make_tensors(names, countries)
        output = classifier(inputs, seq_lengths)
        loss = criterion(output, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        if i % 10 == 0:
        print(f'[{time_since(start)}] Epoch {epoch} ', end='')
        print(f'[{i * len(inputs)}/{len(trainset)}] ', end='')
        print(f'loss={total_loss / (i * len(inputs))}')
    return total_loss

4.6 Implementation – Testing

测试代码如下:

def testModel():
    correct = 0
    total = len(testset)
    print("evaluating trained model ...")
    with torch.no_grad():
        for i, (names, countries) in enumerate(testloader, 1):
            inputs, seq_lengths, target = make_tensors(names, countries)
            output = classifier(inputs, seq_lengths)
            pred = output.max(dim=1, keepdim=True)[1]
            correct += pred.eq(target.view_as(pred)).sum().item()
        percent = '%.2f' % (100 * correct / total)
        print(f'Test set: Accuracy {correct}/{total} {percent}%')
    return correct / total

输出结果图如下:

5 Exercise 13-1 Sentiment Analysis on Movie Reviews

对电影影评做情感分析。

ref:https://www.kaggle.com/c/sentiment-analysis-on-movie-reviews/data

数据集如下:

 

练习之后会解答。

说明:记录学习笔记,如果错误欢迎指正!写文章不易,转载请联系我。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值