利用人名语言分类案例演示RNN、LSTM和GRU的区别(基于PyTorch)
文章目录
- 一、程序结构
- 1.1 程序整体结构
- 1.2 各模块功能关系流程图
- 二、数据预处理模块详解
- 2.1 定义字符集和语言类别
- 2.2 读取数据
- 2.3 人名转换为one-hot编码张量
- 2.4 自定义数据集类
- 2.5 数据加载器
- 三、模型定义模块详解
- 3.1 RNN模型
- 3.2 LSTM模型
- 3.3 GRU模型
- 四、模型训练与测试模块详解
- 4.1 测试模型基本功能
- 4.2 模型训练主函数
- 五、结果可视化与对比模块详解
- 六、模型预测模块详解
- 七、案例结果分析与模型对比
- 7. 1 本次案例结果
- 7.2 结果分析
- 7.3 与经典理论差异的原因
- 7.4 三种模型的特点对比
- 八、完整代码
在自然语言处理领域,人名分类是一个有趣且实用的任务,例如可以根据人名推测其所属的语言或文化背景。本文将详细介绍如何使用 PyTorch 构建基于 RNN、LSTM 和 GRU 的人名分类模型,通过对代码的剖析,帮助大家理解这些循环神经网络在序列数据处理中的应用。
RNN家族介绍:网页链接
一、程序结构
1.1 程序整体结构
test.py
├── 导入依赖库
│ ├── json - JSON序列化/反序列化
│ ├── torch - PyTorch框架
│ ├── torch.nn - 神经网络模块
│ ├── torch.optim- 优化器模块
│ ├── DataLoader/Dataset - 数据加载工具
│ ├── string - 字符操作
│ ├── time - 时间记录
│ ├── matplotlib.pyplot - 可视化绘图
│ └── tqdm - 进度条显示
│
├── 数据预处理模块
│ ├── string_setting() - 定义字符集与语言类别
│ ├── read_data(path) - 读取并清洗训练数据
│ ├── word_to_tensor(x) - 将姓名编码为one-hot张量
│ └── class MyDatasets(Dataset) - 自定义PyTorch数据集类
│ ├── __init__
│ ├── __len__
│ └── __getitem__
│
├── 模型定义模块
│ ├── RNN - 简单循环神经网络
│ │ ├── __init__
│ │ ├── forward
│ │ └── init_hidden
│ │
│ ├── LSTM - 长短期记忆网络
│ │ ├── __init__
│ │ ├── forward
│ │ └── init_hidden
│ │
│ └── GRU - 门控循环单元
│ ├── __init__
│ ├── forward
│ └── init_hidden
│
├── 模型训练与测试模块
│ ├── test_def(*args) - 测试模型前向传播
│ └── my_train_def(*args, n=100, m=1000)
│ ├── 训练流程控制
│ ├── 参数初始化
│ ├── 训练主循环
│ │ ├── 轮次迭代(epoch)
│ │ ├── 批次训练(batch)
│ │ ├── 前向传播、计算损失
│ │ ├── 反向传播、参数更新
│ │ └── 日志统计与保存模型
│ └── 保存训练日志到JSON文件
│
├── 结果可视化与对比模块
│ └── compare_results()
│ ├── 绘制 loss 曲线图(RNN / LSTM / GRU)
│ ├── 绘制 accuracy 曲线图
│ └── 绘制训练时间柱状图
│
├── 模型预测模块
│ └── predict_def(x, *args, t=3)
│ ├── 输入预处理(one-hot转换)
│ ├── 加载训练好的模型参数
│ └── 输出 top-k 预测结果
│
└── 主程序入口└── if __name__ == '__main__':├── 超参数设置├── 初始化字符集 & 语言类别├── 构建模型字典 model_dict = {'RNN': RNN, 'LSTM': LSTM, 'GRU': GRU}├── 开始训练三个模型├── 绘图比较结果(loss.png / acc.png / time.png)└── 对输入姓名进行预测示例
1.2 各模块功能关系流程图
[数据准备] ↓
[模型定义]↓
[模型训练]↓
[训练评估]↓
[结果对比]↓
[新样本预测]
二、数据预处理模块详解
2.1 定义字符集和语言类别
def string_setting():"""定义模型所需的字符集和语言类别标签1. 构建包含字母和常用标点的字符集2. 定义目标语言类别列表(共18种语言)"""# 构建字符集:包含26个大写字母、26个小写字母和常用标点符号al_letters = string.ascii_letters + " .,;'"n_letters = len(al_letters) # 计算字符集大小(57个字符)print("字符数量为:", n_letters)# 定义目标语言类别列表(按顺序:意大利语、英语、阿拉伯语等18种语言)categorys = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese','Vietnamese', 'Japanese', 'French', 'Greek', 'Dutch', 'Korean', 'Polish','Portuguese', 'Russian', 'Czech', 'German']categorys_len = len(categorys) # 计算语言类别数量(18类)print("类别数量为:", categorys_len)return al_letters, categorys # 返回字符集和语言类别列表
代码解析:
- 字符集构建:
string.ascii_letters
包含所有大小写字母,加上常用标点符号(空格、逗号、句号、分号、单引号),共57个字符。这些字符基本覆盖了大多数语言人名中的特殊符号。 - 语言类别:定义了18种目标语言,这些语言的人名在拼写和结构上有明显差异,适合作为分类任务的目标。
2.2 读取数据
def read_data(path):"""从文本文件中读取人名分类数据参数:path (str): 数据文件路径,文件格式为每行"人名\t语言标签"返回:data_list_x (list): 人名列表(x数据集)data_list_y (list): 语言标签列表(y数据集)"""data_list_x, data_list_y = [], [] # 初始化数据存储列表with open(path, encoding="utf-8") as f: # 以UTF-8编码打开文件for line in f.readlines(): # 逐行读取数据# 数据清洗:过滤长度小于等于5的行(可能是无效数据)if len(line) <= 5:continue# 按制表符分割每行数据,前半部分为人名,后半部分为语言标签x, y = line.strip().split('\t')data_list_x.append(x) # 保存人名到x列表data_list_y.append(y) # 保存标签到y列表return data_list_x, data_list_y # 返回清洗后的数据
代码解析:
- 文件读取:使用
with open
语句确保文件资源正确关闭,指定encoding="utf-8"
处理多语言字符。 - 数据清洗:过滤长度小于等于5的行,避免无效数据(如空行或格式错误的行)影响模型训练。
- 数据分割:假设文件格式为"人名\t语言标签",使用
split('\t')
分割每行数据。
2.3 人名转换为one-hot编码张量
def word_to_tensor(x):"""将人名转换为one-hot编码张量参数:x (str): 输入人名(如"John")返回:tensor_x (torch.Tensor): 二维one-hot张量,形状为[len(x), len(al_letters)]示例:输入"John",输出形状为[4, 57]的张量,每个字符在对应位置置1"""tensor_x = torch.zeros(len(x), len(al_letters)) # 初始化全零张量for i, letter in enumerate(x): # 遍历人名中的每个字符# 在字符集中查找当前字符的索引,并在对应位置置1tensor_x[i][al_letters.find(letter)] = 1return tensor_x # 返回one-hot编码张量
代码解析:
- 张量初始化:创建一个形状为
[len(x), len(al_letters)]
的全零张量,其中len(x)
是人名的字符数,len(al_letters)
是字符集大小(57)。 - 字符编码:遍历人名中的每个字符,使用
al_letters.find(letter)
查找字符在字符集中的索引,将对应位置的值设为1。 - 为什么使用one-hot编码:人名是字符序列,每个字符是离散的类别,one-hot编码是表示离散类别最直接的方式,适合作为模型输入。
2.4 自定义数据集类
# 自定义数据集类,继承PyTorch的Dataset基类
class MyDatasets(Dataset):def __init__(self, data_list_x, data_list_y):"""初始化数据集参数:data_list_x (list): 人名列表data_list_y (list): 语言标签列表"""self.data_list_x = data_list_x # 保存人名数据self.data_list_y = data_list_y # 保存标签数据self.data_list_len = len(data_list_x) # 数据集长度def __len__(self):"""返回数据集大小"""return self.data_list_lendef __getitem__(self, index):"""获取指定索引的样本参数:index (int): 样本索引返回:tensor_x (torch.Tensor): one-hot编码的人名张量tensor_y (torch.Tensor): 语言标签的张量表示"""# 处理索引越界:确保索引在有效范围内index = min(max(index, -self.data_list_len), self.data_list_len - 1)x = self.data_list_x[index] # 获取人名y = self.data_list_y[index] # 获取标签# 将人名转换为one-hot张量tensor_x = word_to_tensor(x)# 将标签转换为张量(使用类别索引,类型为long)tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)return tensor_x, tensor_y # 返回样本和标签
代码解析:
- 继承
Dataset
类:PyTorch的Dataset
是所有自定义数据集的基类,必须实现__len__
和__getitem__
方法。 - 索引处理:
__getitem__
方法处理正负索引,确保索引在有效范围内。 - 标签转换:将语言标签转换为对应的类别索引(如"English"对应索引1),使用
torch.long
类型(即int64
),这是PyTorch分类任务中标签的标准类型。
2.5 数据加载器
def data_loader():"""封装数据加载全流程返回:my_dataloader (DataLoader): 数据加载器对象"""path = './name_classfication.txt' # 数据文件路径my_list_x, my_list_y = read_data(path) # 读取数据print(f'x数据集数量为:{len(my_list_x)}') # 打印数据规模print(f'y数据集数量为:{len(my_list_y)}')# 实例化自定义数据集my_dataset = MyDatasets(my_list_x, my_list_y)# 实例化数据加载器:batch_size=1(单样本批量),shuffle=True(打乱数据顺序)my_dataloader = DataLoader(my_dataset, batch_size=1, shuffle=True)return my_dataloader # 返回数据加载器
代码解析:
- 数据加载流程:读取数据文件,创建自定义数据集,再用
DataLoader
封装。 - batch_size=1:人名长度不一,难以组成批量(除非使用padding),因此每次只处理一个样本。
- shuffle=True:打乱数据顺序,增加训练的随机性,避免模型学习到数据的顺序特征。
三、模型定义模块详解
3.1 RNN模型
class RNN(nn.Module):"""简单循环神经网络模型,用于处理序列数据(人名)并分类语言"""def __init__(self, input_size, hidden_size, output_size, batch_size, num_layers):"""初始化RNN模型参数:input_size (int): 输入维度(字符集大小,57)hidden_size (int): 隐藏层维度(128)output_size (int): 输出维度(语言类别数,18)batch_size (int): 批量大小(1)num_layers (int): RNN层数(1)"""super(RNN, self).__init__() # 继承父类初始化self.input_size = input_size # 输入维度self.hidden_size = hidden_size # 隐藏层维度self.output_size = output_size # 输出维度self.batch_size = batch_size # 批量大小self.num_layers = num_layers # RNN层数# 定义RNN层:batch_first=True表示输入格式为[batch, seq, feature]self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)# 定义输出层:将隐藏状态映射到语言类别空间self.output_layer = nn.Linear(hidden_size, output_size)# 定义LogSoftmax层:计算多分类概率(适用于NLLLoss)self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input, hidden):"""前向传播过程参数:input (torch.Tensor): 输入张量,形状[batch, seq, input_size]hidden (torch.Tensor): 隐藏状态,形状[num_layers, batch, hidden_size]返回:output (torch.Tensor): 分类概率,形状[batch, output_size]hn (torch.Tensor): 新的隐藏状态,形状[num_layers, batch, hidden_size]"""xn, hn = self.rnn(input, hidden) # RNN前向传播# 取最后一个时间步的隐藏状态(捕获序列整体特征)tem_ten = xn[:, -1, :]# 通过线性层映射到类别空间output = self.output_layer(tem_ten)# 计算分类概率output = self.softmax(output)return output, hn # 返回输出和新隐藏状态def init_hidden(self):"""初始化隐藏状态为全零张量"""return torch.zeros(self.num_layers, self.batch_size, self.hidden_size)
代码解析:
- 输入维度:
input_size=57
,对应one-hot编码的字符集大小。 - 隐藏层维度:
hidden_size=128
,决定了模型的表示能力。 - RNN层:
nn.RNN
是PyTorch的基础RNN实现,batch_first=True
表示输入格式为[batch, seq, feature]
。 - 输出层:将最后一个时间步的隐藏状态映射到18个语言类别。
- LogSoftmax层:将线性层的输出转换为对数概率分布,配合
NLLLoss
使用。 - 为什么取最后一个时间步:人名是一个序列,最后一个时间步的隐藏状态包含了整个序列的信息,适合用于分类。
3.2 LSTM模型
class LSTM(nn.Module):"""长短期记忆网络模型,解决RNN的长期依赖问题"""def __init__(self, input_size, hidden_size, output_size, batch_size, num_layers):"""初始化LSTM模型参数与RNN类似,新增记忆单元状态"""super(LSTM, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.num_layers = num_layersself.batch_size = batch_sizeself.output_size = output_size# 定义LSTM层:包含隐藏状态和记忆单元self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)self.output_layer = nn.Linear(hidden_size, output_size)self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input, hidden):"""LSTM前向传播参数:input (torch.Tensor): 输入张量hidden (tuple): 包含隐藏状态(h0)和记忆单元(c0)"""h0, c0 = hidden # 解包隐藏状态和记忆单元# LSTM前向传播,返回输出序列和新的隐藏状态xn, (hn, cn) = self.lstm(input, (h0, c0))# 取最后一个时间步的隐藏状态tem_ten = xn[:, -1, :]# 映射到类别空间并计算概率output = self.output_layer(tem_ten)output = self.softmax(output)return output, (hn, cn) # 返回输出和新的隐藏状态、记忆单元def init_hidden(self):"""初始化隐藏状态和记忆单元为全零张量"""h0 = torch.zeros(self.num_layers, self.batch_size, self.hidden_size)c0 = torch.zeros(self.num_layers, self.batch_size, self.hidden_size)return h0, c0 # 返回元组(隐藏状态, 记忆单元)
代码解析:
- LSTM与RNN的区别:LSTM引入了记忆单元
c
,通过门控机制解决了RNN的长期依赖问题。 - 双重状态:LSTM的隐藏状态包含
h
(短期记忆)和c
(长期记忆),初始化时需要分别创建。 - 门控机制:LSTM内部的输入门、遗忘门和输出门控制信息的流动,使模型能够学习长期依赖关系。
3.3 GRU模型
class GRU(nn.Module):"""门控循环单元模型,介于RNN和LSTM之间的简化结构"""def __init__(self, input_size, hidden_size, output_size, batch_size, num_layers):"""初始化GRU模型,结构类似RNN但内部使用门控机制"""super(GRU, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.output_size = output_sizeself.batch_size = batch_sizeself.num_layers = num_layers# 定义GRU层:包含更新门和重置门self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True)self.output_layer = nn.Linear(hidden_size, output_size)self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input, hidden):"""GRU前向传播,流程类似RNN但隐藏状态更新方式不同"""xn, hn = self.gru(input, hidden)tem_ten = xn[:, -1, :]output = self.output_layer(tem_ten)output = self.softmax(output)return output, hn # 返回输出和新隐藏状态def init_hidden(self):"""初始化GRU隐藏状态为全零张量"""return torch.zeros(self.num_layers, self.batch_size, self.hidden_size)
代码解析:
- GRU结构:GRU是LSTM的简化版本,只有两个门(更新门和重置门),计算效率更高。
- 更新门:控制前一时间步的信息有多少被保留到当前时间步。
- 重置门:控制忽略前一时间步的信息程度。
- 适用场景:在序列较短或计算资源有限的情况下,GRU通常比LSTM表现更好。
四、模型训练与测试模块详解
4.1 测试模型基本功能
def test_def(*args):"""测试模型基本功能(前向传播)参数:*args (str): 模型名称列表('RNN', 'LSTM', 'GRU')"""for name in args: # 遍历每个模型名称# 根据模型名称从字典中获取模型类并实例化my_model = model_dict[name](input_size, hidden_size, output_size, batch_size, num_layers)print(f'我的{model_dict[name]}模型:', my_model) # 打印模型结构mydatasets = data_loader() # 获取数据加载器# 前向传播测试(遍历一个批次)for i, (tx, ty) in enumerate(mydatasets):output, hidden = my_model(tx, my_model.init_hidden())if i >= 1: break # 只测试一个样本# 打印输出结果和隐藏状态信息print(f'model_name:{name},output:{output}')print(f'model_name:{name},output.shape:{output.shape}')print(f'model_name:{name},hidden:{hidden}')print(f'model_name:{name},hidden.shape:{hidden[0].shape if type(hidden) == tuple else hidden.shape}')
代码解析:
- 模型实例化:从模型字典中获取模型类并创建实例,验证模型是否能正确初始化。
- 前向传播测试:通过一个样本的前向传播,检查模型的输入输出格式是否正确。
- 形状验证:打印输出和隐藏状态的形状,确保与预期一致(输出应为[1, 18],隐藏状态应为[1, 1, 128])。
4.2 模型训练主函数
def my_train_def(*args, n=100, m=1000):"""模型训练主函数参数:*args (str): 要训练的模型名称列表n (int): 记录损失的迭代间隔m (int): 打印日志的迭代间隔"""n, m = int(n), int(m) # 转换参数类型for name in args: # 遍历每个模型my_datalooder = data_loader() # 获取数据加载器# 实例化模型my_model = model_dict[name](input_size, hidden_size, output_size, batch_size, num_layers)print(f'我的{model_dict[name]}模型:', my_model)# 定义损失函数:负对数似然损失(适用于多分类问题)my_loss_fn = nn.NLLLoss()# 定义优化器:Adam优化器,学习率my_lrmy_adam = optim.Adam(my_model.parameters(), lr=my_lr)# 训练日志参数初始化start_time = time.time() # 记录开始时间total_iter_num = 0 # 总迭代次数total_loss = 0.0 # 总损失total_loss_list = [] # 损失记录列表total_acc_num = 0 # 总正确预测数total_acc_list = [] # 准确率记录列表m_loss = 0.0 # 每m次迭代的损失和m_acc = 0.0 # 每m次迭代的正确数和# 训练主循环:epochs轮次for epoch in range(epochs):print('第%d轮训练开始...' % (epoch + 1)) # 打印轮次信息# 遍历数据加载器中的每个批次(tqdm显示进度条)for tx, ty in tqdm(my_datalooder):h0 = my_model.init_hidden() # 初始化隐藏状态# 前向传播xn, hn = my_model(tx, h0)# 计算损失my_loss = my_loss_fn(xn, ty)# 梯度清零my_adam.zero_grad()# 反向传播my_loss.backward()# 参数更新my_adam.step()# 累计统计信息total_iter_num += 1total_loss += my_loss.item()m_loss += my_loss.item()# 计算当前批次预测是否正确(argmax获取最大概率索引)i_predit_tag = 1 if torch.argmax(xn).item() == ty.item() else 0total_acc_num += i_predit_tagm_acc += i_predit_tag# 每n次迭代记录平均损失和准确率if (total_iter_num % n) == 0:tmploss = total_loss / total_iter_numtotal_loss_list.append(tmploss)tmpacc = total_acc_num / total_iter_numtotal_acc_list.append(tmpacc)# 每m次迭代打印训练日志if (total_iter_num % m) == 0:tmploss = total_loss / total_iter_numtmpacc = total_acc_num / total_iter_numtmp_m_loss = m_loss / mtmp_m_acc = m_acc / mm_loss = 0.0m_acc = 0# 打印详细训练信息(轮次、迭代次数、损失、准确率、时间)print(f'轮次:{epoch + 1},迭代次数:{total_iter_num},总损失:{tmploss:.4f},当前损失:{tmp_m_loss:.4f},'f'总准确率:{tmpacc:.4f},当前准确率:{tmp_m_acc:.4f},时间:{time.time() - start_time:.4f}s')# 每轮次结束保存模型参数torch.save(my_model.state_dict(), f'{path}/{name}+{epoch + 1}.pth')total_time = time.time() - start_time # 计算总时间print(f'总时间:{total_time:.4f}s')# 计算本轮次平均损失和准确率total_loss = total_loss / total_iter_numprint(f'总损失:{total_loss:.4f}')total_acc = total_acc_num / total_iter_numprint(f'测试集准确率:{total_acc:.4f}')# 保存训练日志到JSON文件(包含轮次、损失、准确率、时间等)lstm_dict = {'epochs': epochs,'total_loss': total_loss,'total_acc': total_acc,'total_time': total_time,'total_loss_list': total_loss_list,'total_acc_list': total_acc_list}with open(f'{path}/{name}_dict.json', 'w', encoding='utf-8') as fw:fw.write(json.dumps(lstm_dict))
代码解析:
- 损失函数:
nn.NLLLoss
(负对数似然损失)适用于多分类问题,配合LogSoftmax
使用。 - 优化器:
Adam
优化器自适应调整学习率,通常比SGD收敛更快。 - 训练循环:
- 前向传播:计算模型输出。
- 损失计算:对比模型预测与真实标签。
- 梯度清零:避免梯度累积。
- 反向传播:计算梯度。
- 参数更新:根据梯度更新模型参数。
- 统计信息:记录损失和准确率,用于监控训练过程。
- 模型保存:每轮保存模型参数,防止训练中断导致数据丢失。
- 日志记录:保存训练过程数据,用于后续分析和可视化。
五、结果可视化与对比模块详解
def compare_results():"""对比不同模型的训练效果(损失、准确率、时间)"""# 读取各模型的训练日志JSON文件with open(f'{path}/RNN_dict.json', 'r', encoding='utf-8') as fr:rnn_dict = json.loads(fr.read())with open(f'{path}/LSTM_dict.json', 'r', encoding='utf-8') as fl:lstm_dict = json.loads(fl.read())with open(f'{path}/GRU_dict.json', 'r', encoding='utf-8') as fg:gru_dict = json.loads(fg.read())# 绘制损失对比图plt.figure(0)plt.plot(rnn_dict['total_loss_list'], label='RNN', color='r') # RNN损失曲线(红色)plt.plot(lstm_dict['total_loss_list'], label='LSTM', color='g') # LSTM损失曲线(绿色)plt.plot(gru_dict['total_loss_list'], label='GRU', color='b') # GRU损失曲线(蓝色)plt.legend(loc='upper left') # 显示图例(左上角)plt.title('Comparison of Losses Among Different Models') # 图表标题plt.savefig(f'{path}/loss.png') # 保存图片plt.show() # 显示图表# 绘制准确率对比图plt.figure(1)plt.plot(rnn_dict['total_acc_list'], label='RNN', color='r')plt.plot(lstm_dict['total_acc_list'], label='LSTM', color='g')plt.plot(gru_dict['total_acc_list'], label='GRU', color='b')plt.legend(loc='upper left')plt.title('Comparison of Accuracy Among Different Models')plt.savefig(f'{path}/acc.png')plt.show()# 绘制训练时间对比图(柱状图)plt.figure(2)x_data = ['RNN', 'LSTM', 'GRU'] # x轴标签y_data = [rnn_dict['total_time'], lstm_dict['total_time'], gru_dict['total_time']] # 时间数据plt.bar(range(len(x_data)), y_data, tick_label=x_data) # 绘制柱状图plt.savefig(f'{path}/time.png')plt.title('Comparison of Losses Among Different Models')plt.show()
代码解析:
- 数据读取:从JSON文件中读取各模型的训练日志。
- 损失对比图:直观展示三种模型的损失下降曲线,评估收敛速度和最终性能。
- 准确率对比图:比较三种模型的准确率变化,分析学习效率。
- 训练时间对比:柱状图展示训练时间差异,LSTM通常最慢,RNN最快。
六、模型预测模块详解
def predict_def(x, *args, t=3):"""使用训练好的模型对新人名进行语言预测参数:x (str): 输入人名(如'John')*args (str): 要使用的模型名称列表t (int): 显示前t个预测结果"""if len(args) == 0: # 如果未指定模型,默认使用所有模型args = ['RNN', 'LSTM', 'GRU']for name in args: # 遍历每个模型# 输入数据预处理:转换为one-hot张量并升维(添加batch维度)input_x = word_to_tensor(x).unsqueeze(0)# 实例化模型并加载训练好的参数my_model = model_dict[name](input_size, hidden_size, output_size, batch_size, num_layers)my_model.load_state_dict(torch.load(f'{path}/{name}+{epochs}.pth'))# 模型预测(不计算梯度,节省内存)with torch.no_grad():rnn_xn, hn = my_model(input_x, my_model.init_hidden())# 获取前t个最高概率的预测结果rnn_topv, topi = rnn_xn.topk(t, 1)print(f'{name}预测结果为:')for i in range(t): # 打印前t个预测结果# topi[0][i]是预测类别的索引,categorys[索引]是对应的语言名称print(f'第{i + 1}可能的预测结果为:{categorys[topi[0][i]]}')
代码解析:
- 输入预处理:将人名转换为one-hot张量,并添加batch维度(
unsqueeze(0)
)。 - 模型加载:实例化模型并加载训练好的参数。
- 预测过程:
with torch.no_grad()
:关闭梯度计算,提高推理速度。topk(t, 1)
:获取概率最高的t个类别及其概率值。
- 结果展示:打印前t个预测结果及其对应的语言类别。
七、案例结果分析与模型对比
7. 1 本次案例结果
案例在本次阿中,我们对同一个输入人名"Otto von Habsburg"(德国人)进行了预测,结果如下:
RNN预测结果为:
第1可能的预测结果为:Russian
第2可能的预测结果为:German
第3可能的预测结果为:KoreanLSTM预测结果为:
第1可能的预测结果为:German
第2可能的预测结果为:Dutch
第3可能的预测结果为:CzechGRU预测结果为:
第1可能的预测结果为:German
第2可能的预测结果为:English
第3可能的预测结果为:Czech
7.2 结果分析
-
GRU表现最佳:
- 正确预测出German作为第一可能性
- 第二可能性English也是相关语言(与German同属日耳曼语系)
- 训练时间最长但准确性最高
-
LSTM表现次之:
- 正确预测出German作为第一可能性
- 但第二、三可能性Dutch和Czech属于不同语系
- 训练时间居中
-
RNN表现最差:
- 将German预测为Russian(错误)
- German仅作为第二可能性
- 训练时间最短但准确性最低
7.3 与经典理论差异的原因
虽然经典理论认为LSTM通常优于GRU,但本次案例中GRU表现最好,可能原因包括:
-
任务特性:
- 人名分类任务序列较短(通常不超过20个字符)
- GRU的门控机制足以捕捉关键特征
- LSTM的复杂结构可能带来过拟合风险
-
数据规模:
- 训练数据量有限(约2万条)
- GRU参数更少,在中小规模数据上泛化能力更强
-
超参数设置:
- 所有模型使用相同的超参数(如隐藏层大小、学习率)
- 可能未充分发挥LSTM的潜力
-
随机性因素:
- 训练过程中的随机初始化
- 数据加载顺序的随机性
7.4 三种模型的特点对比
模型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
RNN | 结构简单,计算效率高 | 存在梯度消失/爆炸问题,难以学习长期依赖 | 短序列任务,对计算资源敏感的场景 |
LSTM | 解决长期依赖问题,学习能力强 | 结构复杂,训练时间长,参数多 | 长序列任务,需要捕捉复杂时间依赖的场景 |
GRU | 计算效率高,参数少,训练速度快 | 长期依赖能力略弱于LSTM | 中短序列任务(如人名分类),平衡计算效率和模型性能的场景 |
八、完整代码
数据集下载:百度网盘链接
注意:
- 请在项目根目录放入数据集文件
- 需要在项目根目录创建data文件夹
完整代码:
import json # 用于JSON格式数据的序列化和反序列化,用于保存和读取训练日志
import torch # PyTorch深度学习框架,提供张量操作和自动微分功能
import torch.nn as nn # 包含神经网络模块和层的子包,用于构建模型
import torch.nn.functional as F # 包含常用神经网络函数(如激活函数、损失函数)
import torch.optim as optim # 包含优化算法(如SGD、Adam),用于模型参数更新
from torch.utils.data import DataLoader, Dataset # 数据加载工具,用于批量处理数据
import string # 包含字符串常量和操作函数,用于定义字符集
import time # 用于记录训练时间,评估模型效率
import matplotlib.pyplot as plt # 数据可视化库,用于绘制损失和准确率曲线
from tqdm import tqdm # 用于生成进度条,显示训练过程# ============================== 数据预处理模块 ==============================
def string_setting():"""定义模型所需的字符集和语言类别标签1. 构建包含字母和常用标点的字符集2. 定义目标语言类别列表(共18种语言)"""# 构建字符集:包含26个大写字母、26个小写字母和常用标点符号al_letters = string.ascii_letters + " .,;'"n_letters = len(al_letters) # 计算字符集大小(57个字符)print("字符数量为:", n_letters)# 定义目标语言类别列表(按顺序:意大利语、英语、阿拉伯语等18种语言)categorys = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese','Vietnamese', 'Japanese', 'French', 'Greek', 'Dutch', 'Korean', 'Polish','Portuguese', 'Russian', 'Czech', 'German']categorys_len = len(categorys) # 计算语言类别数量(18类)print("类别数量为:", categorys_len)return al_letters, categorys # 返回字符集和语言类别列表def read_data(path):"""从文本文件中读取人名分类数据参数:path (str): 数据文件路径,文件格式为每行"人名\t语言标签"返回:data_list_x (list): 人名列表(x数据集)data_list_y (list): 语言标签列表(y数据集)"""data_list_x, data_list_y = [], [] # 初始化数据存储列表with open(path, encoding="utf-8") as f: # 以UTF-8编码打开文件for line in f.readlines(): # 逐行读取数据# 数据清洗:过滤长度小于等于5的行(可能是无效数据)if len(line) <= 5:continue# 按制表符分割每行数据,前半部分为人名,后半部分为语言标签x, y = line.strip().split('\t')data_list_x.append(x) # 保存人名到x列表data_list_y.append(y) # 保存标签到y列表return data_list_x, data_list_y # 返回清洗后的数据def word_to_tensor(x):"""将人名转换为one-hot编码张量参数:x (str): 输入人名(如"John")返回:tensor_x (torch.Tensor): 二维one-hot张量,形状为[len(x), len(al_letters)]示例:输入"John",输出形状为[4, 57]的张量,每个字符在对应位置置1"""tensor_x = torch.zeros(len(x), len(al_letters)) # 初始化全零张量for i, letter in enumerate(x): # 遍历人名中的每个字符# 在字符集中查找当前字符的索引,并在对应位置置1tensor_x[i][al_letters.find(letter)] = 1return tensor_x # 返回one-hot编码张量# 自定义数据集类,继承PyTorch的Dataset基类
class MyDatasets(Dataset):def __init__(self, data_list_x, data_list_y):"""初始化数据集参数:data_list_x (list): 人名列表data_list_y (list): 语言标签列表"""self.data_list_x = data_list_x # 保存人名数据self.data_list_y = data_list_y # 保存标签数据self.data_list_len = len(data_list_x) # 数据集长度def __len__(self):"""返回数据集大小"""return self.data_list_lendef __getitem__(self, index):"""获取指定索引的样本参数:index (int): 样本索引返回:tensor_x (torch.Tensor): one-hot编码的人名张量tensor_y (torch.Tensor): 语言标签的张量表示"""# 处理索引越界:确保索引在有效范围内index = min(max(index, -self.data_list_len), self.data_list_len - 1)x = self.data_list_x[index] # 获取人名y = self.data_list_y[index] # 获取标签# 将人名转换为one-hot张量tensor_x = word_to_tensor(x)# 将标签转换为张量(使用类别索引,类型为long)tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)return tensor_x, tensor_y # 返回样本和标签def data_loader():"""封装数据加载全流程返回:my_dataloader (DataLoader): 数据加载器对象"""path = './name_classfication.txt' # 数据文件路径my_list_x, my_list_y = read_data(path) # 读取数据print(f'x数据集数量为:{len(my_list_x)}') # 打印数据规模print(f'y数据集数量为:{len(my_list_y)}')# 实例化自定义数据集my_dataset = MyDatasets(my_list_x, my_list_y)# 实例化数据加载器:batch_size=1(单样本批量),shuffle=True(打乱数据顺序)my_dataloader = DataLoader(my_dataset, batch_size=1, shuffle=True)return my_dataloader # 返回数据加载器# ============================== 模型定义模块 ==============================
class RNN(nn.Module):"""简单循环神经网络模型,用于处理序列数据(人名)并分类语言"""def __init__(self, input_size, hidden_size, output_size, batch_size, num_layers):"""初始化RNN模型参数:input_size (int): 输入维度(字符集大小,57)hidden_size (int): 隐藏层维度(128)output_size (int): 输出维度(语言类别数,18)batch_size (int): 批量大小(1)num_layers (int): RNN层数(1)"""super(RNN, self).__init__() # 继承父类初始化self.input_size = input_size # 输入维度self.hidden_size = hidden_size # 隐藏层维度self.output_size = output_size # 输出维度self.batch_size = batch_size # 批量大小self.num_layers = num_layers # RNN层数# 定义RNN层:batch_first=True表示输入格式为[batch, seq, feature]self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)# 定义输出层:将隐藏状态映射到语言类别空间self.output_layer = nn.Linear(hidden_size, output_size)# 定义LogSoftmax层:计算多分类概率(适用于NLLLoss)self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input, hidden):"""前向传播过程参数:input (torch.Tensor): 输入张量,形状[batch, seq, input_size]hidden (torch.Tensor): 隐藏状态,形状[num_layers, batch, hidden_size]返回:output (torch.Tensor): 分类概率,形状[batch, output_size]hn (torch.Tensor): 新的隐藏状态,形状[num_layers, batch, hidden_size]"""xn, hn = self.rnn(input, hidden) # RNN前向传播# 取最后一个时间步的隐藏状态(捕获序列整体特征)tem_ten = xn[:, -1, :]# 通过线性层映射到类别空间output = self.output_layer(tem_ten)# 计算分类概率output = self.softmax(output)return output, hn # 返回输出和新隐藏状态def init_hidden(self):"""初始化隐藏状态为全零张量"""return torch.zeros(self.num_layers, self.batch_size, self.hidden_size)class LSTM(nn.Module):"""长短期记忆网络模型,解决RNN的长期依赖问题"""def __init__(self, input_size, hidden_size, output_size, batch_size, num_layers):"""初始化LSTM模型参数与RNN类似,新增记忆单元状态"""super(LSTM, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.num_layers = num_layersself.batch_size = batch_sizeself.output_size = output_size# 定义LSTM层:包含隐藏状态和记忆单元self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)self.output_layer = nn.Linear(hidden_size, output_size)self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input, hidden):"""LSTM前向传播参数:input (torch.Tensor): 输入张量hidden (tuple): 包含隐藏状态(h0)和记忆单元(c0)"""h0, c0 = hidden # 解包隐藏状态和记忆单元# LSTM前向传播,返回输出序列和新的隐藏状态xn, (hn, cn) = self.lstm(input, (h0, c0))# 取最后一个时间步的隐藏状态tem_ten = xn[:, -1, :]# 映射到类别空间并计算概率output = self.output_layer(tem_ten)output = self.softmax(output)return output, (hn, cn) # 返回输出和新的隐藏状态、记忆单元def init_hidden(self):"""初始化隐藏状态和记忆单元为全零张量"""h0 = torch.zeros(self.num_layers, self.batch_size, self.hidden_size)c0 = torch.zeros(self.num_layers, self.batch_size, self.hidden_size)return h0, c0 # 返回元组(隐藏状态, 记忆单元)class GRU(nn.Module):"""门控循环单元模型,介于RNN和LSTM之间的简化结构"""def __init__(self, input_size, hidden_size, output_size, batch_size, num_layers):"""初始化GRU模型,结构类似RNN但内部使用门控机制"""super(GRU, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.output_size = output_sizeself.batch_size = batch_sizeself.num_layers = num_layers# 定义GRU层:包含更新门和重置门self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True)self.output_layer = nn.Linear(hidden_size, output_size)self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input, hidden):"""GRU前向传播,流程类似RNN但隐藏状态更新方式不同"""xn, hn = self.gru(input, hidden)tem_ten = xn[:, -1, :]output = self.output_layer(tem_ten)output = self.softmax(output)return output, hn # 返回输出和新隐藏状态def init_hidden(self):"""初始化GRU隐藏状态为全零张量"""return torch.zeros(self.num_layers, self.batch_size, self.hidden_size)# ============================== 模型训练与测试模块 ==============================
def test_def(*args):"""测试模型基本功能(前向传播)参数:*args (str): 模型名称列表('RNN', 'LSTM', 'GRU')"""for name in args: # 遍历每个模型名称# 根据模型名称从字典中获取模型类并实例化my_model = model_dict[name](input_size, hidden_size, output_size, batch_size, num_layers)print(f'我的{model_dict[name]}模型:', my_model) # 打印模型结构mydatasets = data_loader() # 获取数据加载器# 前向传播测试(遍历一个批次)for i, (tx, ty) in enumerate(mydatasets):output, hidden = my_model(tx, my_model.init_hidden())if i >= 1: break # 只测试一个样本# 打印输出结果和隐藏状态信息print(f'model_name:{name},output:{output}')print(f'model_name:{name},output.shape:{output.shape}')print(f'model_name:{name},hidden:{hidden}')print(f'model_name:{name},hidden.shape:{hidden[0].shape if type(hidden) == tuple else hidden.shape}')def my_train_def(*args, n=100, m=1000):"""模型训练主函数参数:*args (str): 要训练的模型名称列表n (int): 记录损失的迭代间隔m (int): 打印日志的迭代间隔"""n, m = int(n), int(m) # 转换参数类型for name in args: # 遍历每个模型my_datalooder = data_loader() # 获取数据加载器# 实例化模型my_model = model_dict[name](input_size, hidden_size, output_size, batch_size, num_layers)print(f'我的{model_dict[name]}模型:', my_model)# 定义损失函数:负对数似然损失(适用于多分类问题)my_loss_fn = nn.NLLLoss()# 定义优化器:Adam优化器,学习率my_lrmy_adam = optim.Adam(my_model.parameters(), lr=my_lr)# 训练日志参数初始化start_time = time.time() # 记录开始时间total_iter_num = 0 # 总迭代次数total_loss = 0.0 # 总损失total_loss_list = [] # 损失记录列表total_acc_num = 0 # 总正确预测数total_acc_list = [] # 准确率记录列表m_loss = 0.0 # 每m次迭代的损失和m_acc = 0.0 # 每m次迭代的正确数和# 训练主循环:epochs轮次for epoch in range(epochs):print('第%d轮训练开始...' % (epoch + 1)) # 打印轮次信息# 遍历数据加载器中的每个批次(tqdm显示进度条)for tx, ty in tqdm(my_datalooder):h0 = my_model.init_hidden() # 初始化隐藏状态# 前向传播xn, hn = my_model(tx, h0)# 计算损失my_loss = my_loss_fn(xn, ty)# 梯度清零my_adam.zero_grad()# 反向传播my_loss.backward()# 参数更新my_adam.step()# 累计统计信息total_iter_num += 1total_loss += my_loss.item()m_loss += my_loss.item()# 计算当前批次预测是否正确(argmax获取最大概率索引)i_predit_tag = 1 if torch.argmax(xn).item() == ty.item() else 0total_acc_num += i_predit_tagm_acc += i_predit_tag# 每n次迭代记录平均损失和准确率if (total_iter_num % n) == 0:tmploss = total_loss / total_iter_numtotal_loss_list.append(tmploss)tmpacc = total_acc_num / total_iter_numtotal_acc_list.append(tmpacc)# 每m次迭代打印训练日志if (total_iter_num % m) == 0:tmploss = total_loss / total_iter_numtmpacc = total_acc_num / total_iter_numtmp_m_loss = m_loss / mtmp_m_acc = m_acc / mm_loss = 0.0m_acc = 0# 打印详细训练信息(轮次、迭代次数、损失、准确率、时间)print(f'轮次:{epoch + 1},迭代次数:{total_iter_num},总损失:{tmploss:.4f},当前损失:{tmp_m_loss:.4f},'f'总准确率:{tmpacc:.4f},当前准确率:{tmp_m_acc:.4f},时间:{time.time() - start_time:.4f}s')# 每轮次结束保存模型参数torch.save(my_model.state_dict(), f'{path}/{name}+{epoch + 1}.pth')total_time = time.time() - start_time # 计算总时间print(f'总时间:{total_time:.4f}s')# 计算本轮次平均损失和准确率total_loss = total_loss / total_iter_numprint(f'总损失:{total_loss:.4f}')total_acc = total_acc_num / total_iter_numprint(f'测试集准确率:{total_acc:.4f}')# 保存训练日志到JSON文件(包含轮次、损失、准确率、时间等)lstm_dict = {'epochs': epochs,'total_loss': total_loss,'total_acc': total_acc,'total_time': total_time,'total_loss_list': total_loss_list,'total_acc_list': total_acc_list}with open(f'{path}/{name}_dict.json', 'w', encoding='utf-8') as fw:fw.write(json.dumps(lstm_dict))# ============================== 结果可视化与对比模块 ==============================
def compare_results():"""对比不同模型的训练效果(损失、准确率、时间)"""# 读取各模型的训练日志JSON文件with open(f'{path}/RNN_dict.json', 'r', encoding='utf-8') as fr:rnn_dict = json.loads(fr.read())with open(f'{path}/LSTM_dict.json', 'r', encoding='utf-8') as fl:lstm_dict = json.loads(fl.read())with open(f'{path}/GRU_dict.json', 'r', encoding='utf-8') as fg:gru_dict = json.loads(fg.read())# 绘制损失对比图plt.figure(0)plt.plot(rnn_dict['total_loss_list'], label='RNN', color='r') # RNN损失曲线(红色)plt.plot(lstm_dict['total_loss_list'], label='LSTM', color='g') # LSTM损失曲线(绿色)plt.plot(gru_dict['total_loss_list'], label='GRU', color='b') # GRU损失曲线(蓝色)plt.legend(loc='upper left') # 显示图例(左上角)plt.title('Comparison of Losses Among Different Models') # 图表标题plt.savefig(f'{path}/loss.png') # 保存图片plt.show() # 显示图表# 绘制准确率对比图plt.figure(1)plt.plot(rnn_dict['total_acc_list'], label='RNN', color='r')plt.plot(lstm_dict['total_acc_list'], label='LSTM', color='g')plt.plot(gru_dict['total_acc_list'], label='GRU', color='b')plt.legend(loc='upper left')plt.title('Comparison of Accuracy Among Different Models')plt.savefig(f'{path}/acc.png')plt.show()# 绘制训练时间对比图(柱状图)plt.figure(2)x_data = ['RNN', 'LSTM', 'GRU'] # x轴标签y_data = [rnn_dict['total_time'], lstm_dict['total_time'], gru_dict['total_time']] # 时间数据plt.bar(range(len(x_data)), y_data, tick_label=x_data) # 绘制柱状图plt.savefig(f'{path}/time.png')plt.title('Comparison of Losses Among Different Models')plt.show()# ============================== 模型预测模块 ==============================
def predict_def(x, *args, t=3):"""使用训练好的模型对新人名进行语言预测参数:x (str): 输入人名(如'John')*args (str): 要使用的模型名称列表t (int): 显示前t个预测结果"""if len(args) == 0: # 如果未指定模型,默认使用所有模型args = ['RNN', 'LSTM', 'GRU']for name in args: # 遍历每个模型# 输入数据预处理:转换为one-hot张量并升维(添加batch维度)input_x = word_to_tensor(x).unsqueeze(0)# 实例化模型并加载训练好的参数my_model = model_dict[name](input_size, hidden_size, output_size, batch_size, num_layers)my_model.load_state_dict(torch.load(f'{path}/{name}+{epochs}.pth'))# 模型预测(不计算梯度,节省内存)with torch.no_grad():rnn_xn, hn = my_model(input_x, my_model.init_hidden())# 获取前t个最高概率的预测结果rnn_topv, topi = rnn_xn.topk(t, 1)print(f'{name}预测结果为:')for i in range(t): # 打印前t个预测结果# topi[0][i]是预测类别的索引,categorys[索引]是对应的语言名称print(f'第{i + 1}可能的预测结果为:{categorys[topi[0][i]]}')if __name__ == '__main__':# 超参数设置my_lr = 1e-3 # 学习率:0.001epochs = 2 # 训练轮次:2轮(教学演示用,实际应增加轮次)al_letters, categorys = string_setting() # 初始化字符集和语言类别input_size = len(al_letters) # 输入维度:57hidden_size = 128 # 隐藏层维度:128num_layers = 1 # 网络层数:1batch_size = 1 # 批量大小:1output_size = len(categorys) # 输出维度:18path = './data' # 数据和模型保存路径# 模型字典:映射模型名称到模型类,便于统一管理和调用model_dict = {'RNN': RNN, 'LSTM': LSTM, 'GRU': GRU}# 执行训练、对比和预测流程my_train_def('RNN', 'LSTM', 'GRU') # 注意:如果同时训练多个模型,可能会导致总训练时间不准(CPU或GPU的性能受运行时间影响较大)compare_results() # 对比模型效果predict_def('Otto von Habsburg') # 对'Otto von Habsburg'(德国人)进行语言预测