利用人名语言分类案例演示RNN、LSTM和GRU的区别(基于PyTorch)

发布于:2025-07-04 ⋅ 阅读:(12) ⋅ 点赞:(0)

在自然语言处理领域,人名分类是一个有趣且实用的任务,例如可以根据人名推测其所属的语言或文化背景。本文将详细介绍如何使用 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):  # 遍历人名中的每个字符
        # 在字符集中查找当前字符的索引,并在对应位置置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 自定义数据集类

# 自定义数据集类,继承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 数据加载器

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_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模型

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 测试模型基本功能

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_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收敛更快。
  • 训练循环
    1. 前向传播:计算模型输出。
    2. 损失计算:对比模型预测与真实标签。
    3. 梯度清零:避免梯度累积。
    4. 反向传播:计算梯度。
    5. 参数更新:根据梯度更新模型参数。
  • 统计信息:记录损失和准确率,用于监控训练过程。
  • 模型保存:每轮保存模型参数,防止训练中断导致数据丢失。
  • 日志记录:保存训练过程数据,用于后续分析和可视化。

五、结果可视化与对比模块详解

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))。
  • 模型加载:实例化模型并加载训练好的参数。
  • 预测过程
    1. with torch.no_grad():关闭梯度计算,提高推理速度。
    2. 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 结果分析

  1. GRU表现最佳

    • 正确预测出German作为第一可能性
    • 第二可能性English也是相关语言(与German同属日耳曼语系)
    • 训练时间最长但准确性最高
  2. LSTM表现次之

    • 正确预测出German作为第一可能性
    • 但第二、三可能性Dutch和Czech属于不同语系
    • 训练时间居中
  3. RNN表现最差

    • 将German预测为Russian(错误)
    • German仅作为第二可能性
    • 训练时间最短但准确性最低

7.3 与经典理论差异的原因

虽然经典理论认为LSTM通常优于GRU,但本次案例中GRU表现最好,可能原因包括:

  1. 任务特性

    • 人名分类任务序列较短(通常不超过20个字符)
    • GRU的门控机制足以捕捉关键特征
    • LSTM的复杂结构可能带来过拟合风险
  2. 数据规模

    • 训练数据量有限(约2万条)
    • GRU参数更少,在中小规模数据上泛化能力更强
  3. 超参数设置

    • 所有模型使用相同的超参数(如隐藏层大小、学习率)
    • 可能未充分发挥LSTM的潜力
  4. 随机性因素

    • 训练过程中的随机初始化
    • 数据加载顺序的随机性

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):  # 遍历人名中的每个字符
        # 在字符集中查找当前字符的索引,并在对应位置置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'(德国人)进行语言预测