基于前馈神经网络的姓氏分类

目录

一、引言

 二、前馈神经网络介绍

2.1 多层感知机(MLP)

2.2 卷积神经网络(CNN)

三、利用前馈神经网络实现姓氏分类

3.1 基于MLP的姓氏分类

3.2 基于CNN的姓氏分类 

四、小结与致谢 

一、引言

在当今全球化和多元文化的背景下,姓氏作为人们身份的重要标识,不仅承载着个体的身份信息,更深刻地反映了各种文化和历史的多样性。然而,面对来自不同语言和文化背景的复杂姓氏数据,传统的分类方法往往面临着诸多挑战。这些挑战包括如何有效处理姓氏的多样性及其隐含的非线性关系,以及如何在大规模数据下保持分类准确性和泛化能力。

在此背景下,利用前馈神经网络作为一种先进的数据建模工具,为解决姓氏分类问题提供了新的可能性和效率。神经网络以其强大的非线性建模能力和自动特征学习能力,能够有效地从复杂的姓氏数据中提取有用的信息,并进行精确的分类。此外,神经网络的灵活性使得可以根据具体任务调整网络结构和参数,从而进一步优化分类性能。

因此,本研究旨在探讨利用前馈神经网络对不同姓氏进行分类国籍的方法。通过在不同文化背景下的实证分析,本研究不仅有助于提升姓氏分类的精度和效率,还为跨学科领域的研究提供了更为准确和全面的数据支持。

 二、前馈神经网络介绍

前馈神经网络(Feedforward Neural Network)是一种基本的人工神经网络模型,被广泛用于各种机器学习任务,包括分类、回归和模式识别等。本文主要介绍两种前馈神经网络:多层感知机(MLP)和卷积神经网络(CNN)。

前馈神经网络一般由多层神经元组成,通常包括输入层、若干隐藏层和输出层。输入层接收原始数据输入,每个输入对应一个特征;隐藏层位于输入层和输出层之间,通过学习权重和偏置来提取输入数据的高级特征;输出层产生最终的预测或分类结果。

在前馈神经网络中,信息从输入层流向输出层,中间没有反馈连接。每个神经元接收来自前一层神经元的输入,并将加权和经过激活函数处理后传递给下一层。

总体而言,前馈神经网络以其能够处理复杂非线性关系、自动学习特征和适应大规模数据等优势,成为现代机器学习中不可或缺的重要组成部分。

2.1 多层感知机(MLP)

我们通常将没有环路或回路的人工神经网络称为前馈网络模型。感知机是一种最简单的前馈网络模型,在感知机模型的基础之上添加隐含层,通常将此类模型称为多层感知机模型(MLP)。

MLP模型中信息处理神经元的激活函数通常为Sigmoid函数。故MLP模型的隐含层可将数据通过非线性映射表示在另一个空间当中,并将模型输出限制在区间(0,1)当中。

Sigmoid函数是一种最常用的激活函数,可将人工神经元的输出限制在区间(0,1)内 

f(x)=Sigmoid(x)=\frac{1}{1+e^{-x}}

通过多层的函数复合可以把原线性不可分的数据映射成线性可分。以输入向量X=(1,x_1,x_2)^T 且隐含层输出向量为h'=(h_1,h_2,h_3)^T的MLP模型为例,使用训练数据集D构造MLP模型,D中原始数据分布情况如左图所示,使用该数据集训练好的MLP模型可通过隐含层将原始数据分布映射为如右图所示的分布。

训练多层感知机通常使用反向传播算法,这是一种基于梯度下降的优化方法,通过调整权重和偏置来最小化预测输出与实际目标之间的误差。多层感知机的结构灵活性使其能够适应各种复杂的数据模式和任务要求,例如分类、回归和模式识别等。 

多层感知机(MLP)的损失函数通常根据任务类型而定。一般用的较多的是交叉熵损失函数(Cross-Entropy Loss):适用于多分类任务,特别是在输出层使用 softmax 激活函数时。

多层感知机(MLP)作为前馈神经网络的一种重要类型,具有以下优缺点:

优点:

1. 适应复杂模式: MLP 可以学习和表示复杂的非线性关系,适合处理多种复杂的机器学习任务,如图像识别、语音识别和自然语言处理等。

2. 灵活的网络结构: 可以通过增加隐藏层和调整每层神经元数量来适应不同的数据模式和任务要求,具有较强的灵活性。

3. 强大的表达能力: 多层结构和非线性激活函数使得 MLP 能够学习并捕捉数据中的复杂特征和模式,从而提高模型的泛化能力。

缺点:

1. 需要大量数据和计算资源:多层感知机的训练通常需要大量的标记数据和计算资源,特别是在增加网络深度和复杂度时。

2. 非凸优化问题:MLP 的训练过程属于非凸优化问题,可能会陷入局部最优解而无法达到全局最优解。

3. 设计和调参复杂性: 需要仔细选择合适的网络结构、激活函数和损失函数,并进行有效的超参数调整,这些过程对初学者来说可能具有一定挑战性。

总体而言,多层感知机因其灵活性和强大的表达能力在机器学习中具有重要地位,但在实际应用中需要综合考虑其优缺点来选择合适的模型和方法。

2.2 卷积神经网络(CNN)

卷积神经网络(Convolutional Neural Networks,CNN)是一种前馈神经网络,是受生物学上感受野(Receptive Field)的机制而提出的,在视觉神经系统中,一个神经元的感受野是指视网膜上的特定区域,只有这个区域内的刺激才能够激活该神经元。

卷积神经网络一般包括以下几个部分:

卷积层:每个卷积层包含多个特征映射, 每个特征映射是一个由多个神经元构成的“平面”, 通过一种卷积滤波器提取的一种特征;

采样层:亦称“池化层”, 其作用是基于局部相关性原理进行亚采样, 从而在减少数据量的同时保留有用信息;

连接层:每个神经元被全连接到上一层每个神经元, 本质就是传统的神经网络, 其目的是通过连接层和输出层的连接完成识别任务;

激活函数:在CNN中通常将 Sigmoid 激活函数替换为修正的线性函数。

卷积神经网络(CNN)具有三个重要的结构特性,它们是:

1. 局部连接:
   - CNN 中的每个神经元并非与上一层的所有神经元连接,而是仅与输入数据的局部区域连接。
   - 这种结构使得网络能够更有效地处理大规模数据,因为它减少了需要训练的参数数量和计算量。

2. 权重共享:
   - 在卷积层中,每个滤波器(卷积核)的参数被用于处理输入的所有位置。
   - 这种共享权重的方式使得网络能够学习到特定的特征检测器,而不管这些特征出现在输入的哪个位置,从而提高了模型的泛化能力并减少了过拟合的风险。

3. 空间或时间上的下采样:
   - 也称为池化(Pooling),这是通过汇聚输入数据的局部区域来减少数据的空间(或时间)维度。
   - 池化操作有助于保持模型对于输入数据的位置变化的不变性(平移不变性),同时减少了计算量和参数数量。

这些特性使得CNN在处理大规模图像和序列数据时具有显著的优势,广泛应用于计算机视觉和自然语言处理等领域。

卷积神经网络(CNN)作为一种特殊的神经网络结构,具有以下优缺点:

优点:

1. 局部连接、权重共享和池化的有效性: 这些特性使得CNN能够高效地处理大规模的图像和序列数据,显著减少了需要训练的参数数量和计算复杂度。

2. 位置不变性:CNN通过权重共享和池化操作,使得模型对于输入数据的位置变化具有一定的不变性,这在处理图像等具有空间结构的数据时特别有用。

3. 适应大规模数据:CNN在处理大规模数据集时表现优异,能够有效地提取和学习复杂的特征,适用于多种计算机视觉和自然语言处理任务。

缺点:

1. 计算资源需求高、需要大量标记数据:尤其是在处理大规模数据和深层网络时,CNN需要大量的计算资源来进行训练和推理,还需要大量的标记数据来达到良好的性能,这在某些应用场景下可能会受限。

2. 不适合序列数据的长距离依赖:尽管在处理时间序列等数据时可以使用一些变种结构,传统的CNN结构对于捕捉长距离依赖关系并不特别有效,这是其在某些任务上的局限性。

3. 可解释性差:CNN作为黑盒模型,其内部特征学习过程和决策依据对人类来说往往难以解释,这在某些应用场景下可能不利。

总体而言,卷积神经网络由于其在图像处理和特征提取方面的优异表现,成为计算机视觉和相关领域中的主流方法之一。在选择使用CNN时,需要综合考虑其优点和缺点,并根据具体任务的需求和限制来进行权衡和调整。

三、利用前馈神经网络实现姓氏分类

3.1 基于MLP的姓氏分类

实验环境:本实验在AutoDL平台上完成,以pytorch框架实现代码

数据集:surnames.csv数据集

这是一个姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,这些姓氏是作者从互联网上不同的姓名来源收集的。该数据集具有一些使其有趣的属性。第一个性质是它是相当不平衡的。排名前三的课程占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。剩下的15个民族的频率也在下降——这也是语言特有的特性。第二个特点是,在国籍和姓氏正字法(拼写)之间有一种有效和直观的关系。有些拼写变体与原籍国联系非常紧密(比如“O ‘Neill”、“Antonopoulos”、“Nagasawa”或“Zhu”)。

代码部分:

导入以下库:

from argparse import Namespace
from collections import Counter
import json
import os
import string

import numpy as np
import pandas as pd

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm_notebook

接下来进行数据矢量化

定义一个名为 Vocabulary 的类,用于处理文本并构建词汇表 

class Vocabulary(object):
    """用于处理文本并提取词汇表的类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        Args:
            token_to_idx (dict): tokens到索引的映射
            add_unk (bool): 一个标志,指示是否添加UNK标记
            unk_token (str): 要添加到词汇表中的UNK标记
        """

        if token_to_idx is None:
            token_to_idx = {}
        self._token_to_idx = token_to_idx

        self._idx_to_token = {idx: token 
                              for token, idx in self._token_to_idx.items()}
        
        self._add_unk = add_unk
        self._unk_token = unk_token
        
        self.unk_index = -1
        if add_unk:
            self.unk_index = self.add_token(unk_token) 
        
    def to_serializable(self):
        """返回一个可序列化的字典"""
        return {'token_to_idx': self._token_to_idx, 
                'add_unk': self._add_unk, 
                'unk_token': self._unk_token}

    @classmethod
    def from_serializable(cls, contents):
        """从序列化的字典实例化词汇表"""
        return cls(**contents)

    def add_token(self, token):
        """根据token更新映射字典

        Args:
            token (str): 要添加到词汇表的项目
        Returns:
            index (int): 与token对应的整数
        """
        try:
            index = self._token_to_idx[token]
        except KeyError:
            index = len(self._token_to_idx)
            self._token_to_idx[token] = index
            self._idx_to_token[index] = token
        return index
    
    def add_many(self, tokens):
        """将一组tokens添加到词汇表
        
        Args:
            tokens (list): 一组字符串tokens
        Returns:
            indices (list): 与tokens对应的索引列表
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """检索与token关联的索引,如果token不存在则返回UNK索引
        
        Args:
            token (str): 要查找的token
        Returns:
            index (int): 与token对应的索引
        Notes:
            对于UNK功能,unk_index需要>=0(已添加到词汇表中)
        """
        if self.unk_index >= 0:
            return self._token_to_idx.get(token, self.unk_index)
        else:
            return self._token_to_idx[token]

    def lookup_index(self, index):
        """返回与索引关联的token
        
        Args: 
            index (int): 要查找的索引
        Returns:
            token (str): 与索引对应的token
        Raises:
            KeyError: 如果索引不在词汇表中
        """
        if index not in self._idx_to_token:
            raise KeyError("索引(%d)不在词汇表中" % index)
        return self._idx_to_token[index]

    def __str__(self):
        return "<Vocabulary(size=%d)>" % len(self)

    def __len__(self):
        return len(self._token_to_idx)

接下来定义一个SurnameVectorizer 类,提供将姓氏数据向量化的功能,通过 surname_vocab 对姓氏进行字符级的 one-hot 编码处理,并支持从数据集或可序列化内容进行实例化,使得在处理姓氏数据时能够方便地转换为机器学习模型所需的输入形式。 

class SurnameVectorizer(object):
    """ 协调词汇表并将其应用的向量化器"""
    def __init__(self, surname_vocab, nationality_vocab):
        """
        Args:
            surname_vocab (Vocabulary): 将字符映射到整数的词汇表
            nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
        """
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab

    def vectorize(self, surname):
        """
        Args:
            surname (str): 姓氏

        Returns:
            one_hot (np.ndarray): 一个折叠的one-hot编码
        """
        vocab = self.surname_vocab
        one_hot = np.zeros(len(vocab), dtype=np.float32)
        for token in surname:
            one_hot[vocab.lookup_token(token)] = 1

        return one_hot

    @classmethod
    def from_dataframe(cls, surname_df):
        """从数据集dataframe实例化向量化器
        
        Args:
            surname_df (pandas.DataFrame): 姓氏数据集
        Returns:
            SurnameVectorizer的一个实例
        """
        surname_vocab = Vocabulary(unk_token="@")
        nationality_vocab = Vocabulary(add_unk=False)

        for index, row in surname_df.iterrows():
            for letter in row.surname:
                surname_vocab.add_token(letter)
            nationality_vocab.add_token(row.nationality)

        return cls(surname_vocab, nationality_vocab)

    @classmethod
    def from_serializable(cls, contents):
        """从可序列化内容实例化向量化器
        
        Args:
            contents (dict): 包含可序列化内容的字典
        Returns:
            SurnameVectorizer的一个实例
        """
        surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
        nationality_vocab =  Vocabulary.from_serializable(contents['nationality_vocab'])
        return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab)

    def to_serializable(self):
        """将向量化器序列化为可保存的内容
        
        Returns:
            dict: 包含可序列化内容的字典
        """
        return {'surname_vocab': self.surname_vocab.to_serializable(),
                'nationality_vocab': self.nationality_vocab.to_serializable()}

 接下来定义一个用于处理姓氏和国籍数据集的PyTorch自定义数据集类 SurnameDataset 和一个生成批次的辅助函数 generate_batches。整合了数据集加载、向量化、分割、批量处理等功能,旨在支持机器学习模型训练过程中的数据准备和批量处理需求。

# 自定义 PyTorch 数据集类用于处理姓氏和国籍数据集
class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        Args:
            surname_df (pandas.DataFrame): 数据集
            vectorizer (SurnameVectorizer): 从数据集实例化的向量化器
        """
        
        # 存储数据集和向量化器
        self.surname_df = surname_df
        self._vectorizer = vectorizer

        # 分割数据集为训练集、验证集和测试集
        self.train_df = self.surname_df[self.surname_df.split=='train']
        self.train_size = len(self.train_df)

        self.val_df = self.surname_df[self.surname_df.split=='val']
        self.validation_size = len(self.val_df)

        self.test_df = self.surname_df[self.surname_df.split=='test']
        self.test_size = len(self.test_df)

        # 用字典存储不同分割对应的数据集和大小
        self._lookup_dict = {'train': (self.train_df, self.train_size),
                             'val': (self.val_df, self.validation_size),
                             'test': (self.test_df, self.test_size)}

        # 初始化为训练数据集
        self.set_split('train')
        
        # 计算类别权重用于不平衡类别处理
        class_counts = surname_df.nationality.value_counts().to_dict()
        def sort_key(item):
            return self._vectorizer.nationality_vocab.lookup_token(item[0])
        sorted_counts = sorted(class_counts.items(), key=sort_key)
        frequencies = [count for _, count in sorted_counts]
        self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)

    @classmethod
    def load_dataset_and_make_vectorizer(cls, surname_csv):
        """加载数据集并从头开始创建新的向量化器
        
        Args:
            surname_csv (str): 数据集的位置
        Returns:
            SurnameDataset的实例
        """
        surname_df = pd.read_csv(surname_csv)
        train_surname_df = surname_df[surname_df.split=='train']
        return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))

    @classmethod
    def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
        """加载数据集和相应的向量化器。用于向量化器已被缓存复用的情况
        
        Args:
            surname_csv (str): 数据集的位置
            vectorizer_filepath (str): 保存的向量化器位置
        Returns:
            SurnameDataset的实例
        """
        surname_df = pd.read_csv(surname_csv)
        vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
        return cls(surname_df, vectorizer)

    @staticmethod
    def load_vectorizer_only(vectorizer_filepath):
        """静态方法,从文件中加载向量化器
        
        Args:
            vectorizer_filepath (str): 序列化向量化器的位置
        Returns:
            SurnameVectorizer的实例
        """
        with open(vectorizer_filepath) as fp:
            return SurnameVectorizer.from_serializable(json.load(fp))

    def save_vectorizer(self, vectorizer_filepath):
        """使用json将向量化器保存到磁盘
        
        Args:
            vectorizer_filepath (str): 保存向量化器的位置
        """
        with open(vectorizer_filepath, "w") as fp:
            json.dump(self._vectorizer.to_serializable(), fp)

    def get_vectorizer(self):
        """返回向量化器"""
        return self._vectorizer

    def set_split(self, split="train"):
        """选择数据集中的特定分割"""
        self._target_split = split
        self._target_df, self._target_size = self._lookup_dict[split]

    def __len__(self):
        return self._target_size

    def __getitem__(self, index):
        """主要的PyTorch数据集处理方法
        
        Args:
            index (int): 数据点的索引
        Returns:
            包含数据点特征(x_surname)和标签(y_nationality)的字典
        """
        row = self._target_df.iloc[index]

        surname_vector = \
            self._vectorizer.vectorize(row.surname)

        nationality_index = \
            self._vectorizer.nationality_vocab.lookup_token(row.nationality)

        return {'x_surname': surname_vector,
                'y_nationality': nationality_index}

    def get_num_batches(self, batch_size):
        """根据批量大小返回数据集中的批次数
        
        Args:
            batch_size (int)
        Returns:
            数据集中的批次数
        """
        return len(self) // batch_size

    
def generate_batches(dataset, batch_size, shuffle=True,
                     drop_last=True, device="cpu"): 
    """
    生成器函数,包装PyTorch DataLoader。确保每个张量位于正确的设备位置。
    """
    dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
                            shuffle=shuffle, drop_last=drop_last)

    for data_dict in dataloader:
        out_data_dict = {}
        for name, tensor in data_dict.items():
            out_data_dict[name] = data_dict[name].to(device)
        yield out_data_dict

 数据处理完毕后开始建立MLP模型

class SurnameClassifier(nn.Module):
    """ 用于对姓氏进行分类的2层多层感知机 """
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): 输入向量的大小
            hidden_dim (int): 第一个线性层的输出大小
            output_dim (int): 第二个线性层的输出大小
        """
        super(SurnameClassifier, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)  # 定义第一个线性层
        self.fc2 = nn.Linear(hidden_dim, output_dim)  # 定义第二个线性层

    def forward(self, x_in, apply_softmax=False):
        """分类器的前向传播
        
        Args:
            x_in (torch.Tensor): 输入数据张量
                x_in.shape 应为 (batch, input_dim)
            apply_softmax (bool): softmax 激活的标志
                如果与交叉熵损失一起使用,应为 False
        Returns:
            结果张量,tensor.shape 应为 (batch, output_dim)
        """
        intermediate_vector = F.relu(self.fc1(x_in))  # 使用ReLU激活函数的第一个线性层
        prediction_vector = self.fc2(intermediate_vector)  # 第二个线性层得到预测向量

        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)  # 若需应用 softmax,对预测向量进行 softmax 处理

        return prediction_vector

开始模型训练部分的搭建

 下述代码定义了一些函数,用于训练神经网络模型并监控其性能,这些函数一起组成了一个训练和监控神经网络模型的基本框架,包括了提前停止机制和模型状态的保存功能,同时提供了一个评估模型性能的指标,即准确率。

def make_train_state(args):
    """创建训练状态参数"""
    return {'stop_early': False,
            'early_stopping_step': 0,
            'early_stopping_best_val': 1e8,
            'learning_rate': args.learning_rate,
            'epoch_index': 0,
            'train_loss': [],
            'train_acc': [],
            'val_loss': [],
            'val_acc': [],
            'test_loss': -1,
            'test_acc': -1,
            'model_filename': args.model_state_file}

def update_train_state(args, model, train_state):
    """处理训练状态的更新。

    组件:
     - 提前停止: 防止过拟合。
     - 模型检查点: 如果模型效果更好,则保存模型

    :param args: 主要参数
    :param model: 要训练的模型
    :param train_state: 表示训练状态值的字典
    :returns:
        新的 train_state
    """

    # 至少保存一个模型
    if train_state['epoch_index'] == 0:
        torch.save(model.state_dict(), train_state['model_filename'])
        train_state['stop_early'] = False

    # 如果表现提高,则保存模型
    elif train_state['epoch_index'] >= 1:
        loss_tm1, loss_t = train_state['val_loss'][-2:]

        # 如果损失恶化
        if loss_t >= train_state['early_stopping_best_val']:
            # 更新步骤
            train_state['early_stopping_step'] += 1
        # 损失减少
        else:
            # 保存最佳模型
            if loss_t < train_state['early_stopping_best_val']:
                torch.save(model.state_dict(), train_state['model_filename'])

            # 重置提前停止步骤
            train_state['early_stopping_step'] = 0

        # 是否提前停止?
        train_state['stop_early'] = \
            train_state['early_stopping_step'] >= args.early_stopping_criteria

    return train_state

def compute_accuracy(y_pred, y_target):
    """计算准确率"""
    _, y_pred_indices = y_pred.max(dim=1)
    n_correct = torch.eq(y_pred_indices, y_target).sum().item()
    return n_correct / len(y_pred_indices) * 100

 然后设置随机种子以及处理相关目录的创建,从而帮助确保训练过程的可复现性和数据保存的有效性

def set_seed_everywhere(seed, cuda):
    np.random.seed(seed)
    torch.manual_seed(seed)
    if cuda:
        torch.cuda.manual_seed_all(seed)

def handle_dirs(dirpath):
    if not os.path.exists(dirpath):
        os.makedirs(dirpath)

 然后对参数进行设定、路径处理以及实现实验的可重现性等设置,为后续训练神经网络模型提供了必要的参数配置和环境准备。

args = Namespace(
    # Data and path information
    surname_csv="data/surnames/surnames_with_splits.csv",
    vectorizer_file="vectorizer.json",
    model_state_file="model.pth",
    save_dir="model_storage/ch4/surname_mlp",
    # Model hyper parameters
    hidden_dim=300,
    # Training hyper parameters
    seed=1337,
    num_epochs=100,
    early_stopping_criteria=5,
    learning_rate=0.001,
    batch_size=64,
    # Runtime options
    cuda=False,
    reload_from_files=False,
    expand_filepaths_to_save_dir=True,
)

# 如果需要扩展文件路径到保存目录
if args.expand_filepaths_to_save_dir:
    args.vectorizer_file = os.path.join(args.save_dir, args.vectorizer_file)
    args.model_state_file = os.path.join(args.save_dir, args.model_state_file)
    
    print("Expanded filepaths: ")
    print("\t{}".format(args.vectorizer_file))
    print("\t{}".format(args.model_state_file))
    
# 检查是否可用 CUDA
if  torch.cuda.is_available():
    args.cuda = True

# 设置设备为 CUDA 或 CPU
args.device = torch.device("cuda" if args.cuda else "cpu")
    
print("Using CUDA: {}".format(args.cuda))

# 设置种子以实现可重现性
set_seed_everywhere(args.seed, args.cuda)

# 处理目录
handle_dirs(args.save_dir)

根据 args.reload_from_files 的取值,决定是重新加载之前保存的模型和数据,还是从头创建新的数据集和词向量化器。接着,获取词向量化器并使用它来初始化一个姓氏分类器,为接下来的模型训练做准备。

if args.reload_from_files:
    # 从检查点重新加载模型和数据
    print("Reloading!")
    dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
                                                              args.vectorizer_file)
else:
    # 创建新的数据集和词向量化器
    print("Creating fresh!")
    dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
    dataset.save_vectorizer(args.vectorizer_file)
    
# 获取词向量化器
vectorizer = dataset.get_vectorizer()

# 创建姓氏分类器
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab), 
                               hidden_dim=args.hidden_dim, 
                               output_dim=len(vectorizer.nationality_vocab))

 接下来进入正式训练过程,下述代码负责训练和验证模型,包含了训练和验证过程中所需的各个步骤,包括模型的前向传播、损失计算、梯度更新、准确率计算、学习率调整和进度条更新。

# 将分类器和数据集权重移到指定的设备上
classifier = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)

# 定义损失函数、优化器和学习率调度器
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
                                                 mode='min', factor=0.5,
                                                 patience=1)

# 初始化训练状态
train_state = make_train_state(args)

# 设置进度条和数据集的划分
epoch_bar = tqdm_notebook(desc='training routine', 
                          total=args.num_epochs,
                          position=0)
dataset.set_split('train')
train_bar = tqdm_notebook(desc='split=train',
                          total=dataset.get_num_batches(args.batch_size), 
                          position=1, 
                          leave=True)
dataset.set_split('val')
val_bar = tqdm_notebook(desc='split=val',
                        total=dataset.get_num_batches(args.batch_size), 
                        position=1, 
                        leave=True)

try:
    for epoch_index in range(args.num_epochs):
        train_state['epoch_index'] = epoch_index

        # 迭代训练数据集

        # 设置批处理生成器,将损失和准确率设为0,并设置为训练模式
        dataset.set_split('train')
        batch_generator = generate_batches(dataset, 
                                           batch_size=args.batch_size, 
                                           device=args.device)
        running_loss = 0.0
        running_acc = 0.0
        classifier.train()

        for batch_index, batch_dict in enumerate(batch_generator):
            # 训练步骤:

            # 1. 清零梯度
            optimizer.zero_grad()

            # 2. 计算输出
            y_pred = classifier(batch_dict['x_surname'])

            # 3. 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)

            # 4. 使用损失计算梯度
            loss.backward()

            # 5. 使用优化器更新参数
            optimizer.step()

            # 计算准确率
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)

            # 更新进度条
            train_bar.set_postfix(loss=running_loss, acc=running_acc, epoch=epoch_index)
            train_bar.update()

        train_state['train_loss'].append(running_loss)
        train_state['train_acc'].append(running_acc)

        # 迭代验证集

        # 设置批处理生成器,并将损失和准确率设为0,设置为评估模式
        dataset.set_split('val')
        batch_generator = generate_batches(dataset, 
                                           batch_size=args.batch_size, 
                                           device=args.device)
        running_loss = 0.
        running_acc = 0.
        classifier.eval()

        for batch_index, batch_dict in enumerate(batch_generator):
            # 计算输出
            y_pred = classifier(batch_dict['x_surname'])

            # 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.to("cpu").item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)

            # 计算准确率
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)
            val_bar.set_postfix(loss=running_loss, acc=running_acc, epoch=epoch_index)
            val_bar.update()

        train_state['val_loss'].append(running_loss)
        train_state['val_acc'].append(running_acc)

        # 更新训练状态
        train_state = update_train_state(args=args, model=classifier,
                                         train_state=train_state)

        scheduler.step(train_state['val_loss'][-1])

        if train_state['stop_early']:
            break

        train_bar.n = 0
        val_bar.n = 0
        epoch_bar.update()
except KeyboardInterrupt:
    print("Exiting loop")

 实际运行情况如下所示:

由于本人租借的是4090系列的显卡,因此训练时间较快,不同配置会有不同的训练速度。

 训练结束后,我们会开始评估训练好的分类器模型在测试数据上的性能,计算损失和准确率,并记录这些指标,以便进一步的模型评估和比较。

# 加载最佳模型参数
classifier.load_state_dict(torch.load(train_state['model_filename']))

# 将模型移动到指定设备上
classifier = classifier.to(args.device)

# 将数据集类别权重也移动到指定设备上
dataset.class_weights = dataset.class_weights.to(args.device)

# 定义交叉熵损失函数
loss_func = nn.CrossEntropyLoss(dataset.class_weights)

# 设置数据集为测试集
dataset.set_split('test')

# 生成测试集的批量数据
batch_generator = generate_batches(dataset, 
                                   batch_size=args.batch_size, 
                                   device=args.device)

running_loss = 0.  # 初始化损失值
running_acc = 0.  # 初始化准确率

# 将分类器设置为评估模式
classifier.eval()

# 遍历测试集的每个批次数据
for batch_index, batch_dict in enumerate(batch_generator):
    # 计算输出
    y_pred = classifier(batch_dict['x_surname'])
    
    # 计算损失
    loss = loss_func(y_pred, batch_dict['y_nationality'])
    loss_t = loss.item()
    running_loss += (loss_t - running_loss) / (batch_index + 1)

    # 计算准确率
    acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
    running_acc += (acc_t - running_acc) / (batch_index + 1)

# 将测试集的损失和准确率保存到训练状态中
train_state['test_loss'] = running_loss
train_state['test_acc'] = running_acc

然后输出模型的测试损失值和准确率

print("Test loss: {};".format(train_state['test_loss']))
print("Test Accuracy: {}".format(train_state['test_acc']))

 结果如下:

 Test loss: 1.7831668090820314; Test Accuracy: 46.31249999999999

接下来我们进行对姓氏分类的预测,我们输入一个姓氏,模型给出预测的国籍

def predict_nationality(surname, classifier, vectorizer):
    """预测一个新的姓氏的国籍
    
    Args:
        surname (str): 要分类的姓氏
        classifier (SurnameClassifer): 分类器的实例
        vectorizer (SurnameVectorizer): 对应的向量化器
    Returns:
        包含最有可能的国籍及其概率的字典
    """
    
    # 将输入的姓氏向量化
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).view(1, -1)
    
    # 使用分类器进行预测
    result = classifier(vectorized_surname, apply_softmax=True)

    # 获取概率最大的标签和概率值
    probability_values, indices = result.max(dim=1)
    index = indices.item()

    # 根据索引查找对应的国籍
    predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
    probability_value = probability_values.item()

    return {'nationality': predicted_nationality, 'probability': probability_value}


new_surname = input("Enter a surname to classify: ")
classifier = classifier.to("cpu")
prediction = predict_nationality(new_surname, classifier, vectorizer)
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

 这里我输入了美国球星科比的姓氏Bryant,模型预测了英国国籍,不过由于美国和英国都是用的英语,所以这也算是预测正确了。

接下里我们还定义了一个predict_topk_nationality函数,预测给定姓氏可能对应的多个国籍,并展示概率最高的前 k 个选项给用户。 

def predict_topk_nationality(name, classifier, vectorizer, k=5):
    # 将输入的名字向量化
    vectorized_name = vectorizer.vectorize(name)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    
    # 使用分类器进行预测
    prediction_vector = classifier(vectorized_name, apply_softmax=True)
    
    # 获取概率值最高的前 k 个国籍
    probability_values, indices = torch.topk(prediction_vector, k=k)
    
    # 从张量中提取数据并转换为 numpy 数组
    probability_values = probability_values.detach().numpy()[0]
    indices = indices.detach().numpy()[0]
    
    results = []
    for prob_value, index in zip(probability_values, indices):
        # 根据索引查找对应的国籍
        nationality = vectorizer.nationality_vocab.lookup_index(index)
        results.append({'nationality': nationality, 
                        'probability': prob_value})
    
    return results


new_surname = input("Enter a surname to classify: ")
classifier = classifier.to("cpu")

k = int(input("How many of the top predictions to see? "))
if k > len(vectorizer.nationality_vocab):
    print("Sorry! That's more than the # of nationalities we have.. defaulting you to max size :)")
    k = len(vectorizer.nationality_vocab)
    
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)

print("Top {} predictions:".format(k))
print("===================")
for prediction in predictions:
    print("{} -> {} (p={:0.2f})".format(new_surname,
                                        prediction['nationality'],
                                        prediction['probability']))

同样的,我还是输入美国球星科比的姓氏Bryant,并要求输出5个概率最高的国籍,预测结果如下:

至此,基于MLP的姓氏分类就结束了,感兴趣的同学可以去尝试复现一下本实验。 

3.2 基于CNN的姓氏分类 

 基于CNN的姓氏分类算法大体上与MLP模型是类似的,只是两者的模型选择不同,但整体算法的框架是相近的,因此我在这里直接给出这段代码的最终的预测效果,感兴趣的同学可以继续深入了解CNN和MLP的异同。

from argparse import Namespace
from collections import Counter
import json
import os
import string

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm_notebook

class Vocabulary(object):
    """用于处理文本并提取词汇以进行映射的类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        Args:
            token_to_idx (dict): 一个预先存在的将标记映射到索引的字典
            add_unk (bool): 一个指示是否添加UNK标记的标志
            unk_token (str): 要添加到词汇表中的UNK标记
        """

        if token_to_idx is None:
            token_to_idx = {}
        self._token_to_idx = token_to_idx

        self._idx_to_token = {idx: token 
                              for token, idx in self._token_to_idx.items()}
        
        self._add_unk = add_unk
        self._unk_token = unk_token
        
        self.unk_index = -1
        if add_unk:
            self.unk_index = self.add_token(unk_token) 
        
        
    def to_serializable(self):
        """返回一个可序列化的字典"""
        return {'token_to_idx': self._token_to_idx, 
                'add_unk': self._add_unk, 
                'unk_token': self._unk_token}

    @classmethod
    def from_serializable(cls, contents):
        """从一个序列化的字典实例化词汇表"""
        return cls(**contents)

    def add_token(self, token):
        """根据标记更新映射字典。

        Args:
            token (str): 要添加到词汇表中的项
        Returns:
            index (int): 与标记对应的整数
        """
        try:
            index = self._token_to_idx[token]
        except KeyError:
            index = len(self._token_to_idx)
            self._token_to_idx[token] = index
            self._idx_to_token[index] = token
        return index
    
    def add_many(self, tokens):
        """将一个标记列表添加到词汇表中
        
        Args:
            tokens (list): 一组字符串标记
        Returns:
            indices (list): 与标记对应的索引列表
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """检索与标记相关联的索引,如果标记不存在则返回UNK索引。
        
        Args:
            token (str): 要查找的标记 
        Returns:
            index (int): 与标记对应的索引
        Notes:
            UNK功能需要unk_index >= 0(已添加到词汇表中)
        """
        if self.unk_index >= 0:
            return self._token_to_idx.get(token, self.unk_index)
        else:
            return self._token_to_idx[token]

    def lookup_index(self, index):
        """返回与索引相关联的标记
        
        Args: 
            index (int): 要查找的索引
        Returns:
            token (str): 与索引对应的标记
        Raises:
            KeyError: 如果索引不在词汇表中
        """
        if index not in self._idx_to_token:
            raise KeyError("索引(%d)不在词汇表中" % index)
        return self._idx_to_token[index]

    def __str__(self):
        return "<Vocabulary(size=%d)>" % len(self)

    def __len__(self):
        return len(self._token_to_idx)

class SurnameVectorizer(object):
    """ 协调词汇表并将其投入使用的向量化器 """

    def __init__(self, surname_vocab, nationality_vocab, max_surname_length):
        """
        Args:
            surname_vocab (Vocabulary): 将字符映射到整数的词汇表
            nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
            max_surname_length (int): 最长姓氏的长度
        """
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab
        self._max_surname_length = max_surname_length

    def vectorize(self, surname):
        """
        Args:
            surname (str): 姓氏
        Returns:
            one_hot_matrix (np.ndarray): 一个独热向量矩阵
        """
        one_hot_matrix_size = (len(self.surname_vocab), self._max_surname_length)
        one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)

        for position_index, character in enumerate(surname):
            character_index = self.surname_vocab.lookup_token(character)
            one_hot_matrix[character_index][position_index] = 1

        return one_hot_matrix

    @classmethod
    def from_dataframe(cls, surname_df):
        """从数据集DataFrame实例化向量化器
        
        Args:
            surname_df (pandas.DataFrame): 姓氏数据集
        Returns:
            SurnameVectorizer的实例
        """
        surname_vocab = Vocabulary(unk_token="@")
        nationality_vocab = Vocabulary(add_unk=False)
        max_surname_length = 0

        for index, row in surname_df.iterrows():
            max_surname_length = max(max_surname_length, len(row.surname))
            for letter in row.surname:
                surname_vocab.add_token(letter)
            nationality_vocab.add_token(row.nationality)

        return cls(surname_vocab, nationality_vocab, max_surname_length)

    @classmethod
    def from_serializable(cls, contents):
        surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
        nationality_vocab = Vocabulary.from_serializable(contents['nationality_vocab'])
        return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab, 
                   max_surname_length=contents['max_surname_length'])

    def to_serializable(self):
        return {
            'surname_vocab': self.surname_vocab.to_serializable(),
            'nationality_vocab': self.nationality_vocab.to_serializable(),
            'max_surname_length': self._max_surname_length
        }

class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        Args:
            name_df (pandas.DataFrame): the dataset
            vectorizer (SurnameVectorizer): vectorizer instatiated from dataset
        """
        self.surname_df = surname_df
        self._vectorizer = vectorizer
        self.train_df = self.surname_df[self.surname_df.split=='train']
        self.train_size = len(self.train_df)

        self.val_df = self.surname_df[self.surname_df.split=='val']
        self.validation_size = len(self.val_df)

        self.test_df = self.surname_df[self.surname_df.split=='test']
        self.test_size = len(self.test_df)

        self._lookup_dict = {'train': (self.train_df, self.train_size),
                             'val': (self.val_df, self.validation_size),
                             'test': (self.test_df, self.test_size)}

        self.set_split('train')
        
        # Class weights
        class_counts = surname_df.nationality.value_counts().to_dict()
        def sort_key(item):
            return self._vectorizer.nationality_vocab.lookup_token(item[0])
        sorted_counts = sorted(class_counts.items(), key=sort_key)
        frequencies = [count for _, count in sorted_counts]
        self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)


    @classmethod
    def load_dataset_and_make_vectorizer(cls, surname_csv):
        """Load dataset and make a new vectorizer from scratch
        
        Args:
            surname_csv (str): location of the dataset
        Returns:
            an instance of SurnameDataset
        """
        surname_df = pd.read_csv(surname_csv)
        train_surname_df = surname_df[surname_df.split=='train']
        return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))

    @classmethod
    def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
        """Load dataset and the corresponding vectorizer. 
        Used in the case in the vectorizer has been cached for re-use
        
        Args:
            surname_csv (str): location of the dataset
            vectorizer_filepath (str): location of the saved vectorizer
        Returns:
            an instance of SurnameDataset
        """
        surname_df = pd.read_csv(surname_csv)
        vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
        return cls(surname_df, vectorizer)

    @staticmethod
    def load_vectorizer_only(vectorizer_filepath):
        """a static method for loading the vectorizer from file
        
        Args:
            vectorizer_filepath (str): the location of the serialized vectorizer
        Returns:
            an instance of SurnameDataset
        """
        with open(vectorizer_filepath) as fp:
            return SurnameVectorizer.from_serializable(json.load(fp))

    def save_vectorizer(self, vectorizer_filepath):
        """saves the vectorizer to disk using json
        
        Args:
            vectorizer_filepath (str): the location to save the vectorizer
        """
        with open(vectorizer_filepath, "w") as fp:
            json.dump(self._vectorizer.to_serializable(), fp)

    def get_vectorizer(self):
        """ returns the vectorizer """
        return self._vectorizer

    def set_split(self, split="train"):
        """ selects the splits in the dataset using a column in the dataframe """
        self._target_split = split
        self._target_df, self._target_size = self._lookup_dict[split]

    def __len__(self):
        return self._target_size

    def __getitem__(self, index):
        """the primary entry point method for PyTorch datasets
        
        Args:
            index (int): the index to the data point 
        Returns:
            a dictionary holding the data point's features (x_data) and label (y_target)
        """
        row = self._target_df.iloc[index]

        surname_matrix = \
            self._vectorizer.vectorize(row.surname)

        nationality_index = \
            self._vectorizer.nationality_vocab.lookup_token(row.nationality)

        return {'x_surname': surname_matrix,
                'y_nationality': nationality_index}

    def get_num_batches(self, batch_size):
        """Given a batch size, return the number of batches in the dataset
        
        Args:
            batch_size (int)
        Returns:
            number of batches in the dataset
        """
        return len(self) // batch_size

    
def generate_batches(dataset, batch_size, shuffle=True,
                     drop_last=True, device="cpu"):
    """
    A generator function which wraps the PyTorch DataLoader. It will 
      ensure each tensor is on the write device location.
    """
    dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
                            shuffle=shuffle, drop_last=drop_last)

    for data_dict in dataloader:
        out_data_dict = {}
        for name, tensor in data_dict.items():
            out_data_dict[name] = data_dict[name].to(device)
        yield out_data_dict

class SurnameClassifier(nn.Module):
    def __init__(self, initial_num_channels, num_classes, num_channels):
        """
        Args:
            initial_num_channels (int): 输入特征向量的大小
            num_classes (int): 输出预测向量的大小
            num_channels (int): 网络中要使用的常量通道大小
        """
        super(SurnameClassifier, self).__init__()
        
        self.convnet = nn.Sequential(
            nn.Conv1d(in_channels=initial_num_channels, 
                      out_channels=num_channels, kernel_size=3),  # 第一层卷积层
            nn.ELU(),
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3, stride=2),  # 第二层卷积层
            nn.ELU(),
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3, stride=2),  # 第三层卷积层
            nn.ELU(),
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3),  # 第四层卷积层
            nn.ELU()
        )
        self.fc = nn.Linear(num_channels, num_classes)  # 全连接层,用于输出最终的预测结果

    def forward(self, x_surname, apply_softmax=False):
        """分类器的前向传播
        
        Args:
            x_surname (torch.Tensor): 输入数据张量。x_surname.shape 应该为 (batch, initial_num_channels, max_surname_length)
            apply_softmax (bool): softmax 激活的标志
                如果与交叉熵损失一起使用,则应为 False
        Returns:
            结果张量。tensor.shape 应该为 (batch, num_classes)
        """
        features = self.convnet(x_surname).squeeze(dim=2)  # 在第2维上压缩张量
        
        prediction_vector = self.fc(features)  # 使用全连接层进行预测

        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)  # 对预测结果进行 softmax 激活

        return prediction_vector

def make_train_state(args):
    return {'stop_early': False,
            'early_stopping_step': 0,
            'early_stopping_best_val': 1e8,
            'learning_rate': args.learning_rate,
            'epoch_index': 0,
            'train_loss': [],
            'train_acc': [],
            'val_loss': [],
            'val_acc': [],
            'test_loss': -1,
            'test_acc': -1,
            'model_filename': args.model_state_file}

def update_train_state(args, model, train_state):
    """处理训练状态更新。

    组件:
     - 提前停止: 防止过拟合。
     - 模型检查点: 如果模型性能更好,则保存模型。

    :param args: 主要参数
    :param model: 要训练的模型
    :param train_state: 代表训练状态值的字典
    :returns:
        新的 train_state
    """

    # 至少保存一个模型
    if train_state['epoch_index'] == 0:
        torch.save(model.state_dict(), train_state['model_filename'])
        train_state['stop_early'] = False

    # 如果模型性能提高,则保存模型
    elif train_state['epoch_index'] >= 1:
        loss_tm1, loss_t = train_state['val_loss'][-2:]

        # 如果损失变差
        if loss_t >= train_state['early_stopping_best_val']:
            # 更新步数
            train_state['early_stopping_step'] += 1
        # 损失减少
        else:
            # 保存最佳模型
            if loss_t < train_state['early_stopping_best_val']:
                torch.save(model.state_dict(), train_state['model_filename'])

            # 重置提前停止步数
            train_state['early_stopping_step'] = 0

        # 是否提前停止?
        train_state['stop_early'] = \
            train_state['early_stopping_step'] >= args.early_stopping_criteria

    return train_state

def compute_accuracy(y_pred, y_target):
    y_pred_indices = y_pred.max(dim=1)[1]
    n_correct = torch.eq(y_pred_indices, y_target).sum().item()
    return n_correct / len(y_pred_indices) * 100

args = Namespace(
    # 数据和路径信息
    surname_csv="data/surnames/surnames_with_splits.csv",
    vectorizer_file="vectorizer.json",
    model_state_file="model.pth",
    save_dir="model_storage/ch4/cnn",
    # 模型超参数
    hidden_dim=100,
    num_channels=256,
    # 训练超参数
    seed=1337,
    learning_rate=0.001,
    batch_size=128,
    num_epochs=100,
    early_stopping_criteria=5,
    dropout_p=0.1,
    # 运行时选项
    cuda=False,
    reload_from_files=False,
    expand_filepaths_to_save_dir=True,
    catch_keyboard_interrupt=True
)


if args.expand_filepaths_to_save_dir:
    args.vectorizer_file = os.path.join(args.save_dir,
                                        args.vectorizer_file)

    args.model_state_file = os.path.join(args.save_dir,
                                         args.model_state_file)
    
    print("扩展后的文件路径: ")
    print("\t{}".format(args.vectorizer_file))
    print("\t{}".format(args.model_state_file))
    
# 检查 CUDA
if torch.cuda.is_available():
    args.cuda = True

args.device = torch.device("cuda" if args.cuda else "cpu")
print("使用 CUDA: {}".format(args.cuda))

def set_seed_everywhere(seed, cuda):
    np.random.seed(seed)
    torch.manual_seed(seed)
    if cuda:
        torch.cuda.manual_seed_all(seed)
        
def handle_dirs(dirpath):
    if not os.path.exists(dirpath):
        os.makedirs(dirpath)
        
# 设置种子以实现重现性
set_seed_everywhere(args.seed, args.cuda)

# 处理目录
handle_dirs(args.save_dir)

if args.reload_from_files:
    # training from a checkpoint
    dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
                                                              args.vectorizer_file)
else:
    # create dataset and vectorizer
    dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
    dataset.save_vectorizer(args.vectorizer_file)
    
vectorizer = dataset.get_vectorizer()

classifier = SurnameClassifier(initial_num_channels=len(vectorizer.surname_vocab), 
                               num_classes=len(vectorizer.nationality_vocab),
                               num_channels=args.num_channels)

classifer = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)

loss_func = nn.CrossEntropyLoss(weight=dataset.class_weights)
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
                                           mode='min', factor=0.5,
                                           patience=1)

train_state = make_train_state(args)

# 设置训练过程的进度条
epoch_bar = tqdm_notebook(desc='training routine', 
                          total=args.num_epochs,
                          position=0)

# 设置训练集的进度条
dataset.set_split('train')
train_bar = tqdm_notebook(desc='split=train',
                          total=dataset.get_num_batches(args.batch_size), 
                          position=1, 
                          leave=True)

# 设置验证集的进度条
dataset.set_split('val')
val_bar = tqdm_notebook(desc='split=val',
                        total=dataset.get_num_batches(args.batch_size), 
                        position=1, 
                        leave=True)

try:
    # 循环每个epoch
    for epoch_index in range(args.num_epochs):
        train_state['epoch_index'] = epoch_index

        # 迭代训练数据集

        # 设置: 创建batch生成器, 将损失和准确率设置为0, 将classifier设置为训练模式
        dataset.set_split('train')
        batch_generator = generate_batches(dataset, 
                                           batch_size=args.batch_size, 
                                           device=args.device)
        running_loss = 0.0
        running_acc = 0.0
        classifier.train()

        # 遍历每个batch
        for batch_index, batch_dict in enumerate(batch_generator):
            # 训练步骤包括以下5步:

            # --------------------------------------
            # 步骤1. 梯度置零
            optimizer.zero_grad()

            # 步骤2. 计算输出
            y_pred = classifier(batch_dict['x_surname'])

            # 步骤3. 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)

            # 步骤4. 使用损失计算梯度
            loss.backward()

            # 步骤5. 使用优化器更新参数
            optimizer.step()
            # -----------------------------------------
            # 计算准确率
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)

            # 更新训练进度条
            train_bar.set_postfix(loss=running_loss, acc=running_acc, 
                            epoch=epoch_index)
            train_bar.update()

        train_state['train_loss'].append(running_loss)
        train_state['train_acc'].append(running_acc)

        # 循环验证数据集

        # 设置: 创建batch生成器, 将损失和准确率设置为0, 将classifier设置为评估模式
        dataset.set_split('val')
        batch_generator = generate_batches(dataset, 
                                           batch_size=args.batch_size, 
                                           device=args.device)
        running_loss = 0.
        running_acc = 0.
        classifier.eval()

        # 遍历每个batch
        for batch_index, batch_dict in enumerate(batch_generator):

            # 计算输出
            y_pred =  classifier(batch_dict['x_surname'])

            # 步骤3. 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)

            # 计算准确率
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)
            val_bar.set_postfix(loss=running_loss, acc=running_acc, 
                            epoch=epoch_index)
            val_bar.update()

        train_state['val_loss'].append(running_loss)
        train_state['val_acc'].append(running_acc)

        train_state = update_train_state(args=args, model=classifier,
                                         train_state=train_state)

        scheduler.step(train_state['val_loss'][-1])

        if train_state['stop_early']:
            break

        train_bar.n = 0
        val_bar.n = 0
        epoch_bar.update()
except KeyboardInterrupt:
    print("Exiting loop")

# 加载模型的状态字典
classifier.load_state_dict(torch.load(train_state['model_filename']))

# 将分类器移动到指定设备上
classifier = classifier.to(args.device)

# 将数据集的类别权重移动到指定设备上
dataset.class_weights = dataset.class_weights.to(args.device)

# 定义交叉熵损失函数,使用数据集中的类别权重
loss_func = nn.CrossEntropyLoss(dataset.class_weights)

# 设置数据集为测试模式
dataset.set_split('test')

# 生成批处理数据
batch_generator = generate_batches(dataset, 
                                   batch_size=args.batch_size, 
                                   device=args.device)

# 初始化运行时损失值和准确率
running_loss = 0.
running_acc = 0.

# 将分类器设置为评估模式
classifier.eval()

# 迭代处理每个批次数据
for batch_index, batch_dict in enumerate(batch_generator):
    # 计算分类器的输出
    y_pred = classifier(batch_dict['x_surname'])
    
    # 计算损失
    loss = loss_func(y_pred, batch_dict['y_nationality'])
    loss_t = loss.item()
    running_loss += (loss_t - running_loss) / (batch_index + 1)

    # 计算准确率
    acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
    running_acc += (acc_t - running_acc) / (batch_index + 1)

# 将测试损失和准确率保存到训练状态中
train_state['test_loss'] = running_loss
train_state['test_acc'] = running_acc


print("Test loss: {};".format(train_state['test_loss']))
print("Test Accuracy: {}".format(train_state['test_acc']))

def predict_nationality(surname, classifier, vectorizer):
    """Predict the nationality from a new surname
    
    Args:
        surname (str): the surname to classifier
        classifier (SurnameClassifer): an instance of the classifier
        vectorizer (SurnameVectorizer): the corresponding vectorizer
    Returns:
        a dictionary with the most likely nationality and its probability
    """
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(0)
    result = classifier(vectorized_surname, apply_softmax=True)

    probability_values, indices = result.max(dim=1)
    index = indices.item()

    predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
    probability_value = probability_values.item()

    return {'nationality': predicted_nationality, 'probability': probability_value}

new_surname = input("Enter a surname to classify: ")
classifier = classifier.cpu()
prediction = predict_nationality(new_surname, classifier, vectorizer)
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

def predict_topk_nationality(surname, classifier, vectorizer, k=5):
    """预测新姓氏的前K个国籍
    
    Args:
        surname (str): 要分类的姓氏
        classifier (SurnameClassifer): 分类器的实例
        vectorizer (SurnameVectorizer): 对应的向量化器
        k (int): 要返回的前K个国籍数
    Returns:
        包含国籍和概率的字典列表
    """
    
    # 将姓氏向量化
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(dim=0)
    
    # 获取预测结果和概率值
    prediction_vector = classifier(vectorized_surname, apply_softmax=True)
    probability_values, indices = torch.topk(prediction_vector, k=k)
    
    # 将概率值和索引转换为numpy数组
    probability_values = probability_values[0].detach().numpy()
    indices = indices[0].detach().numpy()
    
    results = []
    for kth_index in range(k):
        # 获取国籍和对应的概率值
        nationality = vectorizer.nationality_vocab.lookup_index(indices[kth_index])
        probability_value = probability_values[kth_index]
        results.append({'nationality': nationality, 
                        'probability': probability_value})
    return results

# 获取用户输入的新姓氏和要展示的前K个预测
new_surname = input("Enter a surname to classify: ")

k = int(input("How many of the top predictions to see? "))
# 如果要展示的预测数超过所支持的国籍数,将其设置为最大支持数
if k > len(vectorizer.nationality_vocab):
    print("Sorry! That's more than the # of nationalities we have.. defaulting you to max size :)")
    k = len(vectorizer.nationality_vocab)
    
# 进行姓氏国籍预测
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)

# 打印前K个预测结果
print("Top {} predictions:".format(k))
print("===================")
for prediction in predictions:
    print("{} -> {} (p={:0.2f})".format(new_surname,
                                        prediction['nationality'],
                                        prediction['probability']))

训练过程: 

模型的损失值和准确率:

Test loss: 1.8183686037858326;
Test Accuracy: 56.77083333333333

 在预测过程中,我同样是选择Bryant作为输入,不过有意思的是,根据模型的准确率来看,CNN模型比MLP模型要更胜一筹,但是在这个预测任务中,CNN却预测错误了,预测为French。

不过在下一环节中,输出多个最有可能的国籍概率后,预测为英国和法国的概率其实只差了0.01

 

至此,基于CNN的姓氏分类实验也结束了,有兴趣的同学同样也可以去复现一下本次实验。

四、小结与致谢 

        本篇文章是本人第一次撰写博客,有许多编写或者解释不到位的地方还请大家多多批评指正,也欢迎评论留言讨论。

        自从上大学以来就一直在CSDN网站上学习计算机相关知识,可以说是对我帮助很大。本学期在我的nlp老师董老师的帮助下,我也尝试在CSDN上撰写博客,在此非常感谢董老师的帮助,并感谢他对本篇文章中理论以及代码部分的教学,同时我也感谢各位同学对我的帮助。谢谢各位

  • 34
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值