文章目录
-
- 一、程序结构
-
- [1.1 程序整体结构](#1.1 程序整体结构)
- [1.2 各模块功能关系流程图](#1.2 各模块功能关系流程图)
- 二、数据预处理模块详解
-
- [2.1 定义字符集和语言类别](#2.1 定义字符集和语言类别)
- [2.2 读取数据](#2.2 读取数据)
- [2.3 人名转换为one-hot编码张量](#2.3 人名转换为one-hot编码张量)
- [2.4 自定义数据集类](#2.4 自定义数据集类)
- [2.5 数据加载器](#2.5 数据加载器)
- 三、模型定义模块详解
-
- [3.1 RNN模型](#3.1 RNN模型)
- [3.2 LSTM模型](#3.2 LSTM模型)
- [3.3 GRU模型](#3.3 GRU模型)
- 四、模型训练与测试模块详解
-
- [4.1 测试模型基本功能](#4.1 测试模型基本功能)
- [4.2 模型训练主函数](#4.2 模型训练主函数)
- 五、结果可视化与对比模块详解
- 六、模型预测模块详解
- 七、案例结果分析与模型对比
-
- [7. 1 本次案例结果](#7. 1 本次案例结果)
- [7.2 结果分析](#7.2 结果分析)
- [7.3 与经典理论差异的原因](#7.3 与经典理论差异的原因)
- [7.4 三种模型的特点对比](#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 定义字符集和语言类别
python
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 读取数据
python
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编码张量
python
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): # 遍历人名中的每个字符
# 在字符集中查找当前字符的索引,并在对应位置置1
tensor_x[i][al_letters.find(letter)] = 1
return 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 自定义数据集类
python
# 自定义数据集类,继承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_len
def __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 数据加载器
python
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模型
python
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模型
python
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_size
self.hidden_size = hidden_size
self.num_layers = num_layers
self.batch_size = batch_size
self.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模型
python
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_size
self.hidden_size = hidden_size
self.output_size = output_size
self.batch_size = batch_size
self.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 测试模型基本功能
python
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 模型训练主函数
python
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_lr
my_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 += 1
total_loss += my_loss.item()
m_loss += my_loss.item()
# 计算当前批次预测是否正确(argmax获取最大概率索引)
i_predit_tag = 1 if torch.argmax(xn).item() == ty.item() else 0
total_acc_num += i_predit_tag
m_acc += i_predit_tag
# 每n次迭代记录平均损失和准确率
if (total_iter_num % n) == 0:
tmploss = total_loss / total_iter_num
total_loss_list.append(tmploss)
tmpacc = total_acc_num / total_iter_num
total_acc_list.append(tmpacc)
# 每m次迭代打印训练日志
if (total_iter_num % m) == 0:
tmploss = total_loss / total_iter_num
tmpacc = total_acc_num / total_iter_num
tmp_m_loss = m_loss / m
tmp_m_acc = m_acc / m
m_loss = 0.0
m_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_num
print(f'总损失:{total_loss:.4f}')
total_acc = total_acc_num / total_iter_num
print(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收敛更快。 - 训练循环 :
- 前向传播:计算模型输出。
- 损失计算:对比模型预测与真实标签。
- 梯度清零:避免梯度累积。
- 反向传播:计算梯度。
- 参数更新:根据梯度更新模型参数。
- 统计信息:记录损失和准确率,用于监控训练过程。
- 模型保存:每轮保存模型参数,防止训练中断导致数据丢失。
- 日志记录:保存训练过程数据,用于后续分析和可视化。
五、结果可视化与对比模块详解
python
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最快。
六、模型预测模块详解
python
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可能的预测结果为:Korean
LSTM预测结果为:
第1可能的预测结果为:German
第2可能的预测结果为:Dutch
第3可能的预测结果为:Czech
GRU预测结果为:
第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文件夹
完整代码:
python
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): # 遍历人名中的每个字符
# 在字符集中查找当前字符的索引,并在对应位置置1
tensor_x[i][al_letters.find(letter)] = 1
return 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_len
def __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_size
self.hidden_size = hidden_size
self.num_layers = num_layers
self.batch_size = batch_size
self.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_size
self.hidden_size = hidden_size
self.output_size = output_size
self.batch_size = batch_size
self.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_lr
my_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 += 1
total_loss += my_loss.item()
m_loss += my_loss.item()
# 计算当前批次预测是否正确(argmax获取最大概率索引)
i_predit_tag = 1 if torch.argmax(xn).item() == ty.item() else 0
total_acc_num += i_predit_tag
m_acc += i_predit_tag
# 每n次迭代记录平均损失和准确率
if (total_iter_num % n) == 0:
tmploss = total_loss / total_iter_num
total_loss_list.append(tmploss)
tmpacc = total_acc_num / total_iter_num
total_acc_list.append(tmpacc)
# 每m次迭代打印训练日志
if (total_iter_num % m) == 0:
tmploss = total_loss / total_iter_num
tmpacc = total_acc_num / total_iter_num
tmp_m_loss = m_loss / m
tmp_m_acc = m_acc / m
m_loss = 0.0
m_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_num
print(f'总损失:{total_loss:.4f}')
total_acc = total_acc_num / total_iter_num
print(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.001
epochs = 2 # 训练轮次:2轮(教学演示用,实际应增加轮次)
al_letters, categorys = string_setting() # 初始化字符集和语言类别
input_size = len(al_letters) # 输入维度:57
hidden_size = 128 # 隐藏层维度:128
num_layers = 1 # 网络层数:1
batch_size = 1 # 批量大小:1
output_size = len(categorys) # 输出维度:18
path = './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'(德国人)进行语言预测