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,我们只要做个索引标签就可以了。
构造函数 (
__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
存储了国家数量。
__getitem__
方法:def __getitem__(self, index): return self.names[index], self.country_dict[self.countries[index]]
__getitem__
方法用于获取指定索引位置的样本。返回一个元组,其中第一个元素是样本的名字,第二个元素是样本对应的国家的索引。
__len__
方法:def __len__(self): return self.len
__len__
方法返回数据集的大小,即样本的数量。
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
返回。
idx2country(self, index)
方法:def idx2country(self, index): return self.country_list[index]
这个方法用于根据给定的国家索引
index
返回对应的国家名字。它通过self.country_list[index]
来获取指定索引位置上的国家名字,并将其返回。
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()