在PyTorch深度学习实践概论笔记12-循环神经网络基础篇中简单介绍了RNN,接下来13讲,我们介绍一个关于神经网络的应用:实现一个循环神经网络的分类器。
用RNN做一个分类器。
先看看这个问题。现在有一个数据集,数据集里有人名和对应的国家,我们需要训练一个模型,输入一个新的名字,模型能预测出这个名字是基于哪种语言的(18种不同的语言,18分类)。
回顾上一讲。
在自然语言处理中,通常的方式:①先把词或字变成一个one-hot向量,one-hot向量维度高,而且过于稀疏,所以一般来说先通过嵌入层(Embed)把one-hot向量转化成低维的稠密向量,②然后经过RNN,隐层的输出不一定和最终要求的目标一致,所以要用一个线性层把输出映射成和我们的要求一致。
我们的需求是输出名字所属的语言分类,我们对O1-O5这些输出是没有要求的,即不需要对所有的隐层输出做线性变换,为了解决这个问题,我们可以把网络简化,如下图所示。
输入向量经过嵌入层之后,输入到RNN,输出最终的隐层状态,最终的隐层状态经过一个线性层,我们分成18个类别,就可以实现名字分类的任务了。
这一讲使用的模型如下:
输入首先经过嵌入层,然后使用GRU模型,然后使用线性层,注意输入的是最后一个隐藏状态的输出,经过变换变成一个18维的输出。
看一下数据,只有两列:Name和Country。注意输入的每一个名字都是一个序列(x1,x2,...xN),而且序列的长短不一样。
首先看一下主要的循环是怎么写的。
代码如下:
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()
回忆训练的基本过程(四步):
准备数据–定义模型–构造损失函数和优化器–训练过程
首先看一下数据上的准备。
①拿到的是字符串,先转变成序列,转成字符列表,列表里面的每一个数就是名字里面的每一个字符。
②接下来做词典,可以用ASCII表,ASCII表是128个字符,我们把字典长度设置成128,求每一个字符对应的ASCII值,拼成我们想要的序列。上图中的最右表中每一个数并不是一个数字,而是一个one-hot向量。例如77,就是一个128维的向量,第77个数的值为1,其他的值都是0。对于Embedding(嵌入层)来说,只要告诉嵌入层第几个维度是1就行了,所以只需要把ASCII值放在这就行了。
序列长短不一应该怎么解决?
③如上图左,每一行是一个序列,我们解决序列长短不一的方法是padding(因为张量必须保证所有的数据都添满,不然就不是张量),如上图右侧,在做一个batch的时候,我们看这一个batch里面哪一个字符串的长度最长,然后把其他字符串填充成和它一样的长度,这样就能保证可以构成一个张量,因为每个维度的数量不一样是没办法构成张量的。
我们需要把各个分类(国家)转成一个分类索引(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:
先看看和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)
下面具体看看什么是双向的神经网络吧。
序列的forward方向流程图(注意forward不是正向传播):
上图的情况x_(N-1)只包含过去的信息,但是有时候在NLP中也需要考虑未来的信息。
序列的backward方向流程图(注意backward不是反向传播):
接着
最后
这样的神经网络称双向的神经网络,backward最后得到h_N^b,输出是上面的h0、h1...hN。
最后输出的hidden只有两个,公式如下:
再接着看,这个地方就需要乘上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
以上内容是模型相应的设定。
接下来看看name转化成tensor的过程。
转化过程如下:
①name转换成一个一个字符,转化成对应的ASCII值。
②填充:
③转置:
④降序排列:
看看make_tensors函数。
把每个字符变成列表。代码如下:
训练:
代码如下:
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
测试代码如下:
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
输出结果图如下:
对电影影评做情感分析。
ref:https://www.kaggle.com/c/sentiment-analysis-on-movie-reviews/data
数据集如下:
练习之后会解答。
说明:记录学习笔记,如果错误欢迎指正!写文章不易,转载请联系我。