深度学习基础12:RNN Classifier
·
# ============= 标准库导入 =============
import csv # 用于读取和写入CSV文件格式
import gzip # 用于读取gzip压缩文件(.gz后缀)
import math # 数学函数库,提供floor等函数
import time # 时间相关功能,用于计算训练时间
# ============= 第三方库导入 =============
import numpy as np # 数值计算库,提供数组操作和数学函数
import matplotlib.pyplot as plt # 绘图库,用于可视化训练结果
# ============= PyTorch核心导入 =============
import torch # PyTorch深度学习框架主库
from torch import nn # 神经网络模块(包含各种层、损失函数等)
from torch.utils.data import Dataset, DataLoader # 数据集处理工具
from torch.nn.utils.rnn import pack_padded_sequence # RNN变长序列处理工具
# 继承torch.utils.data.Dataset,用于创建自定义数据集
class NameDataset(Dataset):
# 构造函数,is_train_set=True表示训练集,False表示测试集
def __init__(self, is_train_set=True):
# 根据is_train_set选择不同的数据文件
filename = 'data/names_train.csv.gz' if is_train_set else 'data/names_test.csv.gz'
# gzip.open打开压缩文件,'rt'表示以文本模式读取
with gzip.open(filename, 'rt') as f:
# csv.reader创建CSV文件读取器
reader = csv.reader(f)
# 将CSV文件的所有行读取到列表中
rows = list(reader)
# 提取每行的第一个元素(名字)到names列表
self.names = [row[0] for row in rows]
# 记录数据集大小
self.len = len(self.names)
# 提取每行的第二个元素(国家)到countries列表
self.countries = [row[1] for row in rows]
# 获取唯一的国家列表并排序
# set(self.countries):获取所有不重复的国家
# sorted():按字母顺序排序
# list():转换为列表
self.country_list = list(sorted(set(self.countries)))
# 调用getCountryDict方法创建国家到索引的映射字典
self.country_dict = self.getCountryDict()
# 记录国家数量
self.country_num = len(self.country_list)
# 用于获取指定索引的数据
# 参数:index - 数据索引(0到len(dataset)-1)
# 返回:名字, 国家索引
def __getitem__(self, index):
# self.countries[index是数据的位置编号,不是国家的类别编号]:获取国家的名称(字符串)
# self.country_dict["国家名"]:查询这个国家名对应的数字索引(返回的是基于国家名称得到有意义的数字编码)
return self.names[index], self.country_dict[self.countries[index]]
# 返回数据集大小
def __len__(self):
return self.len
# 创建国家名称到索引的映射字典 {'国家名': 索引}
def getCountryDict(self):
# 创建空字典
country_dict = dict()
# enumerate(iterable, start=0):返回索引和元素
# idx从0开始,country_name是国家名称
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
# 计算从since开始经过的时间
# 参数:since - 起始时间(time.time()的返回值)
def time_since(since):
s = time.time() - since
m = math.floor(s / 60)
s -= m * 60
return '%dm %ds' % (m, s)
# 继承torch.nn.Module,定义神经网络模型
class RNNClassifier(torch.nn.Module):
# 构造函数
def __init__(self,
input_size, # input_size: 输入维度(字符数量)
hidden_size, # hidden_size: 隐藏层维度
output_size, # output_size: 输出维度
n_layers=1, # n_layers: RNN层数=1
bidirectional=True): # bidirectional: 是否双向(True)
# 调用父类nn.Module的构造函数
super(RNNClassifier, self).__init__()
# 保存隐藏层大小
self.hidden_size = hidden_size
# 保存RNN层数
self.n_layers = n_layers
# 计算方向数量:双向为2,单向为1
self.n_directions = 2 if bidirectional else 1
# 嵌入层:将字符索引(0-127)转换为100维向量
# input_size: 词汇表大小(128,ASCII字符数量)
# hidden_size: 嵌入维度(100)
self.embedding = torch.nn.Embedding(input_size, hidden_size)
# GRU层:门控循环单元
# hidden_size: 输入特征维度(100)
# hidden_size: 隐藏状态维度(100)
# n_layers: 堆叠的GRU层数(2)
# bidirectional: 是否双向(True)。如果是双向,输出维度为hidden_size * 2
self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers, bidirectional=bidirectional)
# 全连接层:将GRU输出映射到分类结果
# hidden_size * self.n_directions: 输入特征维度(单向:100,双向:200)
# output_size: 输出维度(国家数量)
self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size)
# 初始化隐藏状态 batch_size:批次大小
def _init_hidden(self, batch_size):
# 创建全零的隐藏状态张量
# 形状:[n_layers * n_directions, batch_size, hidden_size]
hidden = torch.zeros(self.n_layers * self.n_directions, batch_size, self.hidden_size)
# 调用create_tensor函数,将张量移动到GPU(如果可用)
return create_tensor(hidden)
# 前向传播函数
# input: 输入张量,形状[batch_size, seq_len]
# seq_lengths: 每个序列的实际长度,形状[batch_size]
def forward(self, input, seq_lengths):
# input shape : B x S → S x B
# .t():转置操作,将形状从[batch_size, seq_len]变为[seq_len, batch_size]
# GRU期望的输入形状:[seq_len, batch_size, input_size]
input = input.t()
# 获取批次大小
batch_size = input.size(1)
# 初始化隐藏状态
hidden = self._init_hidden(batch_size)
# 通过嵌入层:将整数索引转换为向量
# 输入:[seq_len, batch_size]
# 输出:[seq_len, batch_size, hidden_size]
embedding = self.embedding(input)
# .cpu():将张量从GPU移动到CPU(如果它在GPU上)
# 因为pack_padded_sequence要求lengths参数在CPU上
seq_lengths_cpu = seq_lengths.cpu() # 创建CPU副本
# pack_padded_sequence:打包变长序列
# embedding: 嵌入后的序列
# seq_lengths_cpu: 每个序列的实际长度(必须在CPU上)
# 作用:去除填充部分,只处理实际有效的序列长度
# 返回值:PackedSequence对象
gru_input = pack_padded_sequence(embedding, seq_lengths_cpu) # 使用CPU副本
# GRU前向传播
# 参数:gru_input: 打包后的输入序列 hidden: 初始隐藏状态
# 返回:output: GRU的输出 hidden: 最后一个时间步的隐藏状态
output, hidden = self.gru(gru_input, hidden)
# 如果是双向GRU,需要拼接最后两个隐藏状态
# hidden形状:[n_layers * n_directions, batch_size, hidden_size]
# hidden[-1]: 最后一层前向的隐藏状态
# hidden[-2]: 最后一层反向的隐藏状态
# torch.cat(..., dim=1): 在特征维度拼接
if self.n_directions == 2:
hidden_cat = torch.cat((hidden[-1], hidden[-2]), dim=1)
else:
# 单向GRU,只取最后一层的隐藏状态
hidden_cat = hidden[-1]
# 全连接层:将隐藏状态映射到分类结果
# 输出形状:[batch_size, output_size]
fc_output = self.fc(hidden_cat)
return fc_output
# 将名字字符串转换为ASCII码列表
def name2list(name):
# ord(c): 返回字符c的ASCII码(整数)
# 列表推导式:将每个字符转换为ASCII码
arr = [ord(c) for c in name]
# 返回:ASCII码列表, 名字长度
return arr, len(arr)
# 将张量移动到GPU(如果可用)
def create_tensor(tensor):
# 如果USE_GPU为True且张量不在GPU上
if USE_GPU and not tensor.is_cuda:
# 创建GPU设备对象
device = torch.device("cuda:0")
# .to(device): 将张量移动到指定设备
tensor = tensor.to(device)
return tensor
# 将名字和国家列表转换为PyTorch张量
# names: 名字列表,如['Smith', 'Garcia', ...]
# countries: 国家索引列表,如[0, 1, ...]
def make_tensors(names, countries):
# 对每个名字调用name2list
# 结果:[(ASCII列表, 长度), (ASCII列表, 长度), ...]
sequences_and_lengths = [name2list(name)for name in names]
# 提取ASCII列表部分
name_sequences = [s1[0] for s1 in sequences_and_lengths]
# 创建长度张量
# torch.LongTensor:创建64位整数张量
seq_lengths = torch.LongTensor([s1[1] for s1 in sequences_and_lengths])
# 将国家列表转换为LongTensor
countries = torch.LongTensor(countries)
# 创建名字序列张量
# torch.zeros: 创建全零张量
# 形状:[batch_size, max_seq_len]
# seq_lengths.max().item(): 获取最大序列长度(标量值)
# dtype=torch.long: 指定为64位整数类型(嵌入层需要)
seq_tensor = torch.zeros(len(name_sequences), seq_lengths.max().item(), dtype=torch.long)
# enumerate(zip(...), 0): 同时遍历两个列表,idx从0开始
# seq: ASCII列表,如[83, 109, 105, 116, 104]
# seq_len: 序列长度,如5
for idx, (seq, seq_len) in enumerate(zip(name_sequences, seq_lengths), 0):
# 将ASCII列表填充到对应位置
# seq_tensor[idx, :seq_len]: 第idx行,前seq_len列
# torch.LongTensor(seq): 将列表转换为张量
seq_tensor[idx, :seq_len] = torch.LongTensor(seq)
# 按长度降序排序
# 参数:dim=0: 在第0维(批次维度)排序 descending=True: 降序排列
# 返回:seq_lengths: 排序后的长度 perm_idx: 排序索引(原始位置→新位置)
seq_lengths, perm_idx = seq_lengths.sort(dim=0, descending=True)
# 使用排序索引重新排列名字序列
seq_tensor = seq_tensor[perm_idx]
# 使用排序索引重新排列国家标签
countries = countries[perm_idx]
# 将三个张量都通过create_tensor处理(可能移动到GPU)
return create_tensor(seq_tensor), create_tensor(seq_lengths),create_tensor(countries)
def trainModel(epoch): # 训练模型
total_loss = 0 # 累计损失
# enumerate(trainloader, 1): 遍历训练数据加载器,i从1开始
# names: 一批名字,如['Smith', 'Garcia', ...]
# countries: 对应的国家索引,如[0, 1, ...]
for i, (names, countries) in enumerate(trainloader, 1):
inputs, seq_lengths, target = make_tensors(names, countries) # 将数据转换为张量
output = classifier(inputs, seq_lengths) # 前向传播,得到预测结果
# 计算损失
# criterion: CrossEntropyLoss
# output: 模型预测,形状[batch_size, num_countries]
# target: 真实标签,形状[batch_size]
loss = criterion(output, target)
optimizer.zero_grad() # 清空梯度缓存(防止梯度累积)
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新模型参数
total_loss += loss.item() # 累加损失值 .item(): 将单元素张量转换为Python数值
if i % 10 == 0: # 每10个批次打印一次
print(f'[{time_since(start)}] Epoch {epoch} ', end='') # 打印训练时间和当前epoch
print(f'[{i * len(inputs)}/{len(trainset)}] ', end='') # 打印已处理的样本数/总样本数 len(inputs): 当前批次大小
print(f'loss={total_loss / (i * len(inputs))}')# 打印平均损失 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)
# 获取预测结果
# output.max(dim=1): 在类别维度找最大值
# dim=1: 在第1维(类别维度)操作
# keepdim=True: 保持维度
# [1]: 返回最大值索引(预测的类别)
pred = output.max(dim=1, keepdim=True)[1]
# 统计正确预测数
# target.view_as(pred): 将target调整为和pred相同的形状
# pred.eq(...): 比较预测和真实值是否相等
# .sum(): 统计True的数量
# .item(): 转换为Python数值
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之间的小数)
#Parameters
HIDDEN_SIZE = 100 # GRU隐藏层维度
BATCH_SIZE = 256 # 批次大小
N_LAYER = 2 # GRU层数
N_EPOCHS = 100 # 训练轮数
N_CHARS = 128 # 字符数量(ASCII字符0-127)
USE_GPU = True # 是否使用GPU
# 创建训练集实例 is_train_set=False:表示这是训练集,从训练文件读取数据
trainset = NameDataset(is_train_set=True)
# 创建训练数据加载器
trainloader = DataLoader(trainset, # 数据集
batch_size=BATCH_SIZE, # 批次大小
shuffle=True) # 每个epoch打乱数据顺序
# 创建测试集实例 is_train_set=False:表示这是测试集,从测试文件读取数据
testset = NameDataset(is_train_set=False)
# 创建测试数据加载器
testloader = DataLoader(testset, # 测试数据集对象
batch_size=BATCH_SIZE, # 批次大小(与训练集一致,BATCH_SIZE=256)
shuffle=False) # 测试集不打乱顺序
N_COUNTRY = trainset.getCountriesNum() # 获取国家数量
if __name__ == '__main__': # 主程序入口(防止被其他模块导入时执行)
classifier = RNNClassifier(N_CHARS, HIDDEN_SIZE, N_COUNTRY, N_LAYER) # 创建RNN分类器实例
if USE_GPU:
device = torch.device("cuda:0")
classifier.to(device)
criterion = nn.CrossEntropyLoss() # 创建交叉熵损失函数
optimizer = torch.optim.Adam(classifier.parameters(), lr=0.001) # 创建Adam优化器
start = time.time() # 记录开始时间
print("Training for %d epochs..." % N_EPOCHS)
acc_list = [] # 存储每个epoch的准确率
for epoch in range(1, N_EPOCHS + 1):
trainModel(epoch) # 训练
acc = testModel() # 测试
acc_list.append(acc) # 记录准确率
epoch = np.arange(1,len(acc_list) + 1, 1) # 创建epoch编号数组:从1到len(acc_list),步长为1
acc_list = np.array(acc_list) # 将准确率列表转换为numpy数组
plt.plot(epoch,acc_list) # 绘制准确率曲线
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.grid() # 显示网格
plt.show() # 显示图形窗口
更多推荐
所有评论(0)