循环神经网络高级篇笔记——B站:刘二大人《PyTorch深度学习实践》

1.RNN Classifier - Name Classification
(1)题目

 (2)思考流程

        我们之前一般首先把自然语言里的字或者词变成one-hot向量,但是由于one-hot向量维度太高比较稀疏,所以我们选择先让其经过一个嵌入层,将其转变为低维的、稠密的向量,隐层的输出不一定和我们要求的结果一致,所以后面会经过一个线性层使结果一致。

        但是在这个试题中,最后要生成一个大的分类,没有必要对所有的隐层作线性变换,所以每一个RNNcell后面的线性层可以省略简化为:

(3) 模型结构

        我们选用GRU作为模型,数据有两列,一列是Name,一列是Country,Name里的名字每一个都是一个序列,有的序列长有的序列短。

 ①主要的循环

1.创建分类器:classifier = RNNClassifier(N_CHARS,HIDDEN_SIZE,N_COUNTRY,N_LAYER)
N_CHARS:字符

HIDDEN_SIZE:隐藏层大小

N_COUNTRY:类别数量

N_LAYER:层数
------------------------------------------------------------------------------------------------

2.配置使用的设备:

if USE GPU:
    device = torch.device("cuda:0")
    classifier.to(device)

根据 USE GPU 变量的值,决定是否将模型放到 GPU 上进行训练。如果 USE GPU 为真(非零),则将设备设置为第一个 CUDA 设备,并将模型移动到该设备上。

--------------------------------------------------------------------------------------------------

3.定义损失函数和优化器:

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

这里使用交叉熵损失函数和 Adam 优化器来进行模型训练。损失函数 CrossEntropyLoss 适用于多类别分类问题。

-------------------------------------------------------------------------------------------------

4.训练模型:

start = time.time()#是在记录当前时间的代码,通常用于计算程序的执行时间。
print("Training for %d epochs..."% N_EPOCHS)
acc_list = []
for epoch in range(1, N_EPOCHS + 1):
    # Train cycle
    trainModel()
    acc = testModel()
    acc_list.append(acc)

在这个循环中,模型将进行多个训练周期(epoch)。每个周期,首先调用 trainModel 方法进行训练,然后使用 testModel 方法进行测试并计算准确率。最后,将准确率存储在 acc_list 列表中。

②准备数据

        先将Name做分离,然后做词典,由于都是英文字符,所以我们可以用ASCII作字典,这些序列长短不一,所以我们要加一个padding,找到最长字符串,将其他字符串添成和它一样长的,接下来是Country,我们只要做个索引标签就可以了。

  1. 构造函数 (__init__):

    def _init__(self, is_train_set=True):
        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)
            rows = list(reader)
        self.names = [row[0] for row in rows]#通过列表推导式将每一行数据中的第一个元素(名字)提取出来,并存储在 self.names 列表中。它将所有样本的名字存储在这个列表中。
        self.len = len(self.names)
        self.countries = [row[1] for row in rows]#通过列表推导式将每一行数据中的第二个元素(国家名字)提取出来,并存储在 self.countries 列表中。它将所有样本对应的国家名字存储在这个列表中。
        self.country_list = list(sorted(set(self.countries)))#将 self.countries 列表转换为集合(set)去除重复的国家名字,然后再转换为列表,并按字母顺序排序。这样可以得到一个包含所有不重复国家名字的有序列表,保存在 self.country_list 变量中。
        self.country_dict = self.getCountryDict()
        self.country_num = len(self.country_list)
    

    set() 是 Python 中的一个内置函数,用于创建一个无序且不重复的集合,在构造函数中,sorted() 是 Python 中的一个内置函数,用于对可迭代对象进行排序操作,并返回一个新的有序列表。根据 is_train_set 参数的值来选择训练集文件或测试集文件。然后使用 gzip 模块打开文件并读取其中的数据。names 列表存储了每个样本的名字,countries 列表存储了每个样本对应的国家名称。len 存储了样本数量,country_list 存储了所有不重复的国家名称,并按字母顺序排序。country_dict 是一个表示国家和对应索引的字典,country_num 存储了国家数量。

  2. __getitem__ 方法:

    def __getitem__(self, index):
        return self.names[index], self.country_dict[self.countries[index]]
    

    __getitem__ 方法用于获取指定索引位置的样本。返回一个元组,其中第一个元素是样本的名字,第二个元素是样本对应的国家的索引。

  3. __len__ 方法:

    def __len__(self):
        return self.len
    

    __len__ 方法返回数据集的大小,即样本的数量。

  4. getCountryDict(self) 方法:

    def getCountryDict(self):
        country_dict = dict()
        for idx, country_name in enumerate(self.country_list, 0):
            country_dict[country_name] = idx
        return country_dict
    

    这个方法用于创建一个表示国家和对应索引的字典,并将其作为结果返回。它使用 enumerate() 函数在循环中同时遍历 self.country_list 列表中的元素和索引。
    在循环中,将每个国家名字 country_name 与索引 idx 关联,并存储在 country_dict 字典中。最后,将 country_dict 返回。

  5. idx2country(self, index) 方法:

    def idx2country(self, index):
        return self.country_list[index]
    

    这个方法用于根据给定的国家索引 index 返回对应的国家名字。它通过 self.country_list[index] 来获取指定索引位置上的国家名字,并将其返回。

  6. getCountriesNum(self) 方法:

    def getCountriesNum(self):
        return self.country_num
    

    这个方法用于返回国家的数量。它简单地返回 self.country_num 的值,该值代表不重复的国家数量。

 ​​​​​​                               ​

 

 ③模型设计

       

1.双向循环神经网络        

        双向循环神经网络(Bidirectional Recurrent Neural Network,Bi-RNN)是一种循环神经网络(RNN)的变体,它在每个时间步骤同时考虑过去和未来的输入信息。

        传统的循环神经网络在处理序列数据时,只考虑了过去的历史信息,并且只有一个循环方向。然而,在某些任务中,未来的信息对于当前的预测也是有用的。为了解决这个问题,双向循环神经网络引入了另一个循环层,该循环层按照相反的时间方向处理输入序列。

具体来说,双向循环神经网络由两个独立的循环层组成,一个按照正常的时间顺序处理输入序列,另一个按照相反的时间顺序处理输入序列。每个循环层都有自己的隐藏状态,它们分别捕捉了过去和未来的上下文信息。在每个时间步骤,双向循环神经网络的输出是两个循环层输出的结合,通常是通过连接、拼接或其他方式进行合并,即正向和反向的隐层做拼接。

2.self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers, bidirectional=bidirectional):创建一个 GRU 循环神经网络层,其中输入大小和隐藏层大小均为 hidden_size,层数为 n_layers,并根据 bidirectional 参数决定是否使用双向。

3.self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size):创建一个全连接层 Linear,由于使用双向循环神经网络,所以将隐藏层状态的大小乘以 n_directions,并将其输出大小设置为 output_size

4.hidden = torch.zeros(self.n_layers * self.n_directions, batch_size, self.hidden_size):创建一个全零的张量 hidden,形状为(层数 *是否双向,批量大小,隐藏层大小),用作隐藏状态的初始值。

-------------------------------------------------------------------------------------------------------------------------

5.input = input.t():将输入数据的维度从 B x S 转置为 S x B,以便与 RNN 模型的输入格式相匹配。

6.batch_size = input.size(1):获取输入数据的批量大小(即样本数量)。input 的形状为 S x B,则 input.size() 的返回值是一个元组 (S, B),其中 S 表示序列的长度,B 表示批量大小(即样本数量)input.size(0)返回序列长度, input.size(1)返回批量大小  。

7.gru_input = pack_padded_sequence(embedding, seq_lengths):将嵌入后的数据和序列长度 seq_lengths 传入 pack_padded_sequence 函数进行填充。

8.hidden_cat = torch.cat([hidden[-1], hidden[-2]], dim=1):将最后两个隐藏状态拼接在一起,沿着维度 1 进行拼接,并保存在 hidden_cat 中。这样做是因为双向模型会有两个方向的隐藏状态输出(正向和反向)。

 

name2list 函数是一个用于将名字转换为序列和长度的辅助函数。 例如,如果将名字 “John” 作为输入传递给 name2list 函数,它将返回 (['j', 'o', 'h', 'n'], 4)

seq_tensor[idx, :seq_len] 表示在 seq_tensor 的第 idx 行的前 seq_len 列进行切片操作。

 (4)代码
import torch
import csv
from torch.utils.data import DataLoader, Dataset
from torch.nn.utils.rnn import pack_padded_sequence
import matplotlib.pyplot as plt
# 输入姓名 输出属于哪个国家
import gzip
import time

HIDDEN_SIZE = 100
BATCH_SIZE = 1024
N_LAYER = 2
N_EPOCHS = 100
N_CHARS = 128
USE_GPU = False
start = time.time()


#   读入数据
class NameDataset(Dataset):
    def __init__(self, is_train_set=True):
        filename = 'names_train.csv' if is_train_set else 'names_test.csv'
        with open(filename, 'rt') as f:
            reader = csv.reader(f)
            rows = list(reader)
        self.names = [row[0] for row in rows]  # 第一列人名
        self.len = len(self.names)  # 名字长度
        self.countries = [row[1] for row in rows]  # 对应国家名
        self.country_list = list(sorted(set(self.countries)))  # 对国家名长度排序
        self.country_dict = self.getCountryDict()  # 构造字典 key:国家名 value:index
        self.country_num = len(self.country_list)  # 国家个数

    def __getitem__(self, index):  # 必须重写__getitem__和__len__方法
        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=True)

N_COUNTRY = trainset.getCountriesNum()


#    模型

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)
        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)  # 如果是双向则维度*2

    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 Batchsize*SeqLen->SeqLen*Batchsize
        input = input.t()  # 矩阵转置
        batch_size = input.size(1)

        hidden = self._init_hidden(batch_size)
        embedding = self.embedding(input)

        # pack them up
        gru_input = pack_padded_sequence(embedding, seq_lengths)  ### make be sorted by descendent  打包变长序列

        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


# 数据处理
def create_tensor(tensor):
    if USE_GPU:
        device = torch.device("cuda:0")
        tensor = tensor.to(device)
    return tensor


def name2list(name):  # 返回ascll值和长度
    arr = [ord(c) for c in name]
    return arr, len(arr)


def make_tensors(names, countries):
    sequences_and_lengths = [name2list(name) for name in names]
    name_sequences = [sl[0] for sl in sequences_and_lengths]  # 名字的ascll值
    seq_lengths = torch.LongTensor([sl[1] for sl in sequences_and_lengths])  # 单独把列表长度拿出来 (名字的长度)
    countries = countries.long()

    # make tensor of name,BatchSize x SeqLen   padding
    seq_tensor = torch.zeros(len(name_sequences), seq_lengths.max()).long()  # 先做一个batchsize*max(seq_lengths)全0的张量
    for idx, (seq, seq_len) in enumerate(zip(name_sequences, seq_lengths), 0):
        seq_tensor[idx, :seq_len] = torch.LongTensor(seq)  # 把数据贴到全0的张量上去

    # sort by length to use pack_padded_sequence
    seq_lengths, perm_idx = seq_lengths.sort(dim=0, descending=True)  # sort返回排完序的序列和对应的index
    seq_tensor = seq_tensor[perm_idx]
    countries = countries[perm_idx]

    return create_tensor(seq_tensor), \
        create_tensor(seq_lengths), \
        create_tensor(countries)


#   训练测试模块
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, countrise) in enumerate(testloader, 1):
            inputs, seq_lengths, target = make_tensors(names, countrise)
            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


def time_since(start):
    """
    计算给定时间戳 `start` 与当前时间之间的时间差
    """
    return time.time() - start


if __name__ == '__main__':
    classifier = RNNClassifier(N_CHARS, HIDDEN_SIZE, N_COUNTRY, N_LAYER)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    if USE_GPU:
        device = torch.device("cuda:0")
        classifier.to(device)

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

    print("Training for %d epochs..." % N_EPOCHS)
    acc_list = []
    epoch_list = []
    for epoch in range(1, N_EPOCHS + 1):
        trainModel()
        acc = testModel()
        acc_list.append(acc)
        epoch_list.append(epoch)
    plt.plot(epoch_list, acc_list)
    plt.ylabel('Accuracy')
    plt.xlabel('epoch')
    plt.grid()
    plt.show()
 (5)结果

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值