笔记来自课程《Pytorch深度学习实践》Lecture 13
部分内容来自博主Biranda的博客
RNN Classifier
实现一个根据名字判断所属国家的分类器,数据如下:
在传统RNN网络结果中,o1...on是作为seq to seq的的序列输出,如下图:
在本题中,由于无法得到序列性质的准确输出结果,而我们的问题范围也仅限于对序列的总体情况进行分类。因此可以将网络简化为如下图所示的情况:
即序列依次经过嵌入层和RNN Cell后得到最终的隐藏状态hn,利用最终的隐藏层状态通过一个线性层来进行一个18分类的多分类任务。由此可以设计如下的模型:
数据特点:
数据中的每个名字,实际上是一个序列,每个字母是序列中的一个输入,处理远比想象中费力;
每个名字长短不一,即序列之间本身的长度是不固定的。
实现代码
1. 导入相关包
import torch
import matplotlib.pyplot as plt
import numpy as np
from torch.utils.data import DataLoader
from torch.utils.data import Dataset
import gzip
import csv
import time
from torch.nn.utils.rnn import pack_padded_sequence
import math
2. main cycle
def time_since(since):
s = time.time() - since
m = math.floor(s / 60)
s -= m*60
return '%dm %ds' % (m, s)
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 = []
for epoch in range(1, N_EPOCHS+1):
#训练模型
trainModel()
#检测模型
acc = testModel()
acc_list.append(acc)
#绘制图像
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()
3. 准备数据
首先,利用ASCII码值,来使得每一个名字中的字符变成数字序列。每一个ASCII码值实际上代表着一个长为128的独热向量,以77为例,即在77处为1,其余全部为0
为保证计算,需要将所有输入的名字填充至等长,即进行padding填充,使之能够成为矩阵(张量):
另外,还要对labels进行索引:
代码如下:
class NameDataset(Dataset):
def __init__(self, is_train_set=True):
#读数据
filename = 'names_train.csv.gz' if is_train_set else 'names_test.csv.gz'
with gzip.open(filename, 'rt') as f:
reader = csv.reader(f)
rows = list(reader)
#数据元组(name,country),将其中的name和country提取出来,并记录数量
self.names = [row[0] for row in rows]
self. len = len(self.names)
self.countries = [row[1] for row in rows]
#将country转换成索引
#列表->集合->排序->列表->字典
self.country_list = list(sorted(set(self.countries)))
self.country_dict = self.getCountryDict()
#获取长度
self.country_num = len(self.country_list)
#获取键值对,country(key)-index(value)
def __getitem__(self, index):
return self.names[index], self.country_dict[self.countries[index]]
def __len__(self):
return self.len
def getCountryDict(self):
country_dict = dict()
for idx,country_name in enumerate(self.country_list, 0):
country_dict[country_name]=idx
return country_dict
#根据索引返回国家名
def idx2country(self, index):
return self.country_list[index]
#返回国家数目
def getCountriesNum(self):
return self.country_num
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 = trainset.getCountriesNum()
4. 模型定义
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
#Embedding层输入 (SeqLen,BatchSize)
#Embedding层输出 (SeqLen,BatchSize,HiddenSize)
#将原先样本总数为SeqLen,批量数为BatchSize的数据,转换为HiddenSize维的向量
self.embedding = torch.nn.Embedding(input_size, hidden_size)
#bidirection用于表示神经网络是单向还是双向
self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers, bidirectional = bidirectional)
#线性层需要*direction
self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size)
def _init_hidden(self):
hidden = torch.zeros(self.n_layers * self.n_directions, batch_size, self.hidden_size)
return create_tensors(hidden)
def forward(self, input, seq_length):
#对input进行转置
input = input.t()
batch_size = input.size(1)
#(n_Layer * nDirections, BatchSize, HiddenSize)
hidden = self._init_hidden(batch_size)
#(SeqLen, BatchSize, HiddenSize)
embedding = self.embedding(input)
#对数据计算过程提速
#需要得到嵌入层的结果(输入数据)及每条输入数据的长度
gru_input = pack_padded_sequence(embedding, seq_length)
output, hidden = self.gru(gru_input, hidden)
#如果是双向神经网络会有h_N^f以及h_1^b两个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
在RNN/LSTM/GRU中,都存在双向神经网络这一结构。
在双向计算过程中,对于序列而言,分别进行
的前向计算和
的反向计算。
则对于同一个而言,有前向计算结果
,以及反向计算结果
,将两者进行连接(Concat)即可得到
经过序列的最终结果
.
对于RNN系列的网络而言,其输出包括output以及hidden两部分。其中的output指的是序列对应输出形成的输出序列。hidden指的是隐含层最终输出结果,在双向网络中即为
代码中的转置效果:
pack_padded_sequence() 用于对模型的计算过程进行加速。其原理在于,由于先前对于长短不一的数据需要填充0,而填充的0本质上不必参与运算,因此可以进行优化。
Embedding变换后的结果如图所示,其中深色部分为实际值为0即padding的部分。这部分可以不用参与运算。
5. 转换张量
为了让数据中的name参与计算。前文中已经说明了需要从名字到字符再到ASCII码值进行转换。之后再对转换成的ASCII码值进行padding,即填充0得到统一大小的矩阵。再对矩阵进行转置,这样每一列为一个序列,最后为进行pack_padded_sequence,还要再对序列按照长度进行降序排序。
#ord()取ASCII码值
def name2list(name):
arr = [ord(c) for c in name]
return arr, len(arr)
def create_tensor(tensor):
if USE_GPU:
device = torch.device("cuda:0")
tensor = tensor.to(device)
return tensor
def make_tensors(names, countries):
sequences_and_length = [name2list(name) for name in names]
#取出所有的列表中每个姓名的ASCII码序列
name_sequences = [s1[0] for s1 in sequences_and_length]
#将列表长度转换为LongTensor
seq_length = torch.LongTensor([s1[1] for s1 in sequences_and_length])
#将整型变为长整型
countries = countries.long()
#做padding
#新建一个全0张量大小为最大长度-当前长度
seq_tensor = torch.zeros(len(name_sequences), seq_lengths.max()).long()
#取出每个序列及其长度idx固定0
for idx, (seq, seq_len) in enumerate(zip(name_sequences, seq_length), 0):
#将序列转化为LongTensor填充至第idx维的0到当前长度的位置
seq_tensor[idx, :seq_len] = torch.LongTensor(seq)
#返回排序后的序列及索引
seq_length, perm_idx = seq_length.sort(dim = 0, descending = True)
seq_tensor = seq_tensor[perm_idx]
countries = countries[perm_idx]
return create_tensor(seq_tensor),
create_tensor(seq_length),
create_tensor(countries)
6. 训练模型
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(train_set)}]', end='')
print(f'loss={total_loss / (i * len(inputs))}')
return total_loss
7. 测试模型
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