Advanced RNN - Pytorch

笔记来自课程《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中,都存在双向神经网络这一结构。

在双向计算过程中,对于序列o_1 {\cdots} o_n而言,分别进行x_1 \to x_n的前向计算和x_n \to x_1​的反向计算。

则对于同一个x_i而言,有前向计算结果h_i^f​,以及反向计算结果h_i^b​,将两者进行连接(Concat)即可得到x_i​经过序列的最终结果h_i​.  

对于RNN系列的网络而言,其输出包括output以及hidden两部分。其中的output指的是序列对应输出h_1 \dots h_n形成的输出序列。hidden指的是隐含层最终输出结果,在双向网络中即为[h_N^f,h_N^b]

代码中的转置效果:

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值