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

 一、什么是姓氏分类


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

二、前馈神经网络 

1.多层感知机

   多层感知机是由多个感知机(Perceptron)组成的神经网络,每个感知机在一个层中接受输入并输出到下一层的感知机。每个感知机实际上是一个带有激活函数的线性变换。多层感知器(MLP)被认为是最基本的神经网络构建模块之一。

 

从下图可以看到,多层感知机层与层之间是全连接的。多层感知机最底层是输入层,中间是隐藏层,最后是输出层。  

其实隐藏层到输出层可以看成是一个多类别的逻辑回归,也即softmax回归,所以输出层的输出就是softmax(W2X1+b2),X1表示隐藏层的输出f(W1X+b1)。W1是权重(也叫连接系数),b1是偏置,激活函数f 可以是常用的sigmoid函数或者tanh函数。 

 

2.卷积神经网络(cnn) 

卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习模型,特别擅长处理具有网格结构的数据,如图像和视频。CNNs 是通过引入卷积层和池化层来提取特征,从而减少参数数量和计算复杂度。这是一种非常适合检测空间子结构(并因此创建有意义的空间子结构)的神经网络。CNNs通过使用少量的权重来扫描输入数据张量来实现这一点。通过这种扫描,它们产生表示子结构检测(或不检测)的输出张量。

1.2.1卷积层

卷积运算是卷积层的基本操作。它通过一个小的权重矩阵(称为卷积核或滤波器)在输入数据上滑动,并计算点积来生成输出数据(称为特征图)。如下图所示:

1.2.1.1卷积核(Filter)


卷积核:卷积核是一个固定大小的小矩阵,例如3x3、5x5等。也可以是三维卷积核,如3x3x3的等等,分别表示卷积核的长度、高度、宽度
权重共享:卷积核的参数在整个输入数据上共享,这减少了模型的参数数量。


1.2.1.2 步幅(Stride)


步幅:步幅决定了卷积核在输入数据上移动的步长。步幅越大,输出特征图的尺寸越小。控制卷积之间的步长。如果步长与核相同,则内核计算不会重叠。另一方面,如果跨度为1,则内核重叠最大。输出张量可以通过增加步幅的方式被有意的压缩来总结信息,如下图所示:

1.2.1.3 填充(Padding)


填充:在输入数据的边缘添加额外的像素,以控制输出特征图的尺寸。常见的填充方式包括 "valid"(无填充)和 "same"(填充以保持输入和输出的尺寸相同)。即使stride和kernel_size允许控制每个计算出的特征值有多大范围,它们也有一个有害的、有时是无意的副作用,那就是缩小特征映射的总大小(卷积的输出)。为了抵消这一点,输入数据张量被人为地增加了长度(如果是一维、二维或三维)、高度(如果是二维或三维)和深度(如果是三维),方法是在每个维度上附加和前置0。这意味着CNN将执行更多的卷积,但是输出形状可以控制,而不会影响所需的核大小、步幅或扩展。下图展示了正在运行的填充。

1.2.1.4膨胀(DILATION)


膨胀卷积核(Dilated Convolutional Kernel)是一种用于增加感受野(receptive field)的卷积操作。在传统的卷积操作中,卷积核以固定的步幅在输入数据上滑动,每次覆盖相邻的区域。而膨胀卷积核通过在卷积核中间插入空洞(dilation),使得卷积核的感受野得到扩大。膨胀卷积核的主要特点是可以在不增加参数量和计算复杂度的情况下,增加卷积操作对输入数据的感知能力。这对于处理具有大范围上下文信息的任务非常有用膨胀控制卷积核如何应用于输入矩阵。在下图中,我们显示,将膨胀从1(默认值)增加到2意味着当应用于输入矩阵时,核的元素彼此之间是两个空格。另一种考虑这个问题的方法是在核中跨跃——在核中的元素或核的应用之间存在一个step size,即存在“holes”。这对于在不增加参数数量的情况下总结输入空间的更大区域是有用的。当卷积层被叠加时,扩张卷积被证明是非常有用的。连续扩张的卷积指数级地增大了“接受域”的大小;即网络在做出预测之前所看到的输入空间的大小。

             

1.2.2激活层


激活层(Activation Layer)是神经网络中的关键组件,它通过引入非线性变换,使模型能够学习和表示复杂的模式和关系。常见的激活函数包括ReLU、Sigmoid、Tanh等。下面我们详细介绍几种常见的激活函数及其在深度学习框架中的实现。

1.2.3池化层


池化层(Pooling Layer)是深度学习神经网络中的一种常见层,用于减少特征图的空间尺寸,从而降低模型的复杂度并且增强模型对输入数据的不变性。常见的池化操作包括最大池化(Max Pooling)和平均池化(Average Pooling)。

1.2.3.1最大池化


最大池化从特定区域内选择最大值作为输出,可以通过固定大小的窗口在输入上滑动并选取每个窗口中的最大值来实现。

1.2.3.2平均池化


平均池化从特定区域内计算平均值作为输出,类似地,可以通过固定大小的窗口在输入上滑动并计算每个窗口中的平均值来实现。

1.2.3.3池化层的作用


减少特征图的空间尺寸,降低模型复杂度,减少参数数量;
提供平移、旋转和缩放不变性,即使输入数据发生小的变化,模型也能够识别出相同的特征;
增加特征的鲁棒性,提高模型的泛化能力。

     

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

3.1任务目的

该实验的主要目的是:训练出一个前馈神经网络模型,让其能够实现姓氏分类。

3.2开发环境

本实验使用的软件为python3.6.7
并且为了实现本文目标,需要使用如下语句导入一些Python库:

from argparse import Namespace
from collections import Counter
import json
import os
import string
 
import collections
import numpy as np
import pandas as pd
import re
 
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
 
import matplotlib.pyplot as plt
 
seed = 1337
 
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)

四、具体代码实现


4.1数据集构建


在第一部分介绍姓氏数据集时,作者提到过该数据集的两个性质:第一个性质是它是相当不平衡的。第二个特点是,在国籍和姓氏正字法(拼写)之间有一种有效和直观的关系。

为了创建最终的数据集,我们执行了几个数据集修改操作。第一个目的是减少这种不平衡——原始数据集中70%以上是俄文,这可能是由于抽样偏差或俄文姓氏的增多。为此,我们通过选择标记为俄语的姓氏的随机子集对这个过度代表的类进行子样本。接下来,我们根据国籍对数据集进行分组,并将数据集分为三个部分:70%到训练数据集,15%到验证数据集,最后15%到测试数据集,以便跨这些部分的类标签分布具有可比性。

# 导入必要的库
import pandas as pd
import numpy as np
from argparse import Namespace
from collections import defaultdict

# 设置参数
args = Namespace(
    raw_dataset_csv="data/surnames/surnames.csv",  # 原始数据集CSV文件路径
    train_proportion=0.7,  # 训练集比例
    val_proportion=0.15,  # 验证集比例
    test_proportion=0.15,  # 测试集比例
    output_munged_csv="data/surnames/surnames_with_splits.csv",  # 输出处理过的CSV文件路径
    seed=1337  # 随机种子,用于结果复现
)

# 读取原始数据
surnames = pd.read_csv(args.raw_dataset_csv, header=0)  # 读取CSV文件,第一行作为列名

print(surnames.head())  # 打印前几行数据查看数据结构

# 按照国籍分组数据
by_nationality = collections.defaultdict(list)  # 创建一个默认列表的字典
for _, row in surnames.iterrows():  # 遍历数据集的每一行
    by_nationality[row.nationality].append(row.to_dict())  # 将每一行作为一个字典添加到对应国籍的列表中

# 创建划分后的数据集
final_list = []  # 创建一个最终列表来保存处理后的数据
np.random.seed(args.seed)  # 设置随机种子
for _, item_list in sorted(by_nationality.items()):  # 遍历字典中的每个键值对
    np.random.shuffle(item_list)  # 随机打乱列表中的数据
    n = len(item_list)  # 获取列表长度
    n_train = int(args.train_proportion * n)  # 计算训练集的数量
    n_val = int(args.val_proportion * n)  # 计算验证集的数量
    n_test = int(args.test_proportion * n)  # 计算测试集的数量

    # 为每个数据点分配一个数据集划分属性
    for item in item_list[:n_train]:
        item['split'] = 'train'  # 标记为训练集
    for item in item_list[n_train:n_train + n_val]:
        item['split'] = 'val'  # 标记为验证集
    for item in item_list[n_train + n_val:]:
        item['split'] = 'test'  # 标记为测试集

    # 将处理后的数据添加到最终列表
    final_list.extend(item_list)

# 将划分后的数据写入文件
final_surnames = pd.DataFrame(final_list)  # 将列表转换为DataFrame

print(final_surnames.split.value_counts())  # 打印每个数据集划分的数量

print(final_surnames.head())  # 打印前几行处理后的数据

# 将处理后的数据写入CSV文件
final_surnames.to_csv(args.output_munged_csv, index=False)  # 写入CSV文件,不保存行索引

4.2MLP模型构建

4.2.1Data Vectorization classes
4.2.1.1The Vocabulary

词汇表,用于将字符映射到整数索引。

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_token (str): 
        """
        # 如果没有提供标记到索引的映射,则初始化为空字典
        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  # 是否添加UNK标记
        self._unk_token = unk_token  # UNK标记的文本
        
        self.unk_index = -1  # UNK标记的索引,初始为-1
        if add_unk:
            self.unk_index = self.add_token(unk_token)  # 添加UNK标记并获取其索引
        
    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]  # 对每个标记调用add_token方法
 
    def lookup_token(self, token):
        """检索与标记关联的索引,如果标记不存在则返回UNK索引。
        
        Args:
            token (str): 要查找的标记
        Returns:
            index (int): 对应于标记的索引
        Notes:
            `unk_index`需要大于等于0(已添加到词汇表)以实现UNK功能
        """
        if self.unk_index >= 0:
            return self._token_to_idx.get(token, self.unk_index)  # 如果标记不存在,返回UNK索引
        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("the index (%d) is not in the Vocabulary" % 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)  # 返回词汇表的大小

4.2.1.2The Vectorizer 

应用词汇表并将姓氏转换为向量。

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)  # 初始化 one-hot 向量
        for token in surname:
            one_hot[vocab.lookup_token(token)] = 1  # 设置对应字符的 one-hot 位
 
        return one_hot
 
    @classmethod
    def from_dataframe(cls, surname_df):
        """从数据集数据框实例化向量器
        
        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):
        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):
        return {'surname_vocab': self.surname_vocab.to_serializable(),  # 序列化姓氏词汇表
                'nationality_vocab': self.nationality_vocab.to_serializable()}  # 序列化国籍词汇表
4.2.1.3The Dataset
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)

        # 保存不同数据集的DataFrame和大小
        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"):
        """根据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):
        """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

 4.2.2The MLP Model:SurnameClassifier

class SurnameClassifier(nn.Module):
    """一个两层的MLP模型,用于分类姓氏"""
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        Args:
            input_dim (int): 输入向量的尺寸
            hidden_dim (int): 第一个线性层(隐藏层)的输出尺寸
            output_dim (int): 第二个线性层(输出层)的输出尺寸
        """
        super(SurnameClassifier, self).__init__()  # 继承nn.Module
        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激活函数
 
        return prediction_vector

4.3CNN模型构建

4.3.1Data Vectorization classes
4.3.1.1The Vocabulary

同4.2.1.1

4.3.1.2The Vectorizer
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向量的矩阵
        """
        # 创建一个one-hot矩阵,其大小为词汇表大小乘以最长姓氏长度
        one_hot_matrix_size = (len(self.surname_vocab), self._max_surname_length)
        one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)
                               
        # 对每个字符,查找其在词汇表中的索引,并在对应的one-hot位置设置为1
        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):
        """从数据框实例化向量器
        
        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}  # 序列化最长姓氏长度

4.3.1.3The Dataset 

和4.2.1.2中不同的是SurnameDataset.getitem()函数。

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()
        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):
        """从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):
        """ 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

 4.3.2The CNN Model:SurnameClassifier 

import torch.nn as nn
import torch.nn.functional as F

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__()  # 继承nn.Module
        
        self.convnet = nn.Sequential(  # 创建一个顺序的神经网络模块
            nn.Conv1d(in_channels=initial_num_channels, 
                      out_channels=num_channels, kernel_size=3),  # 第一个卷积层
            nn.ELU(),  # ELU激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3, stride=2),  # 第二个卷积层,步长为2
            nn.ELU(),  # ELU激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3, stride=2),  # 第三个卷积层,步长为2
            nn.ELU(),  # ELU激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3),  # 第四个卷积层
            nn.ELU()  # 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)  # 卷积网络的输出,然后通过挤压操作去除最后一个维度
       
        prediction_vector = self.fc(features)  # 通过线性层得到预测向量
 
        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)  # 如果需要,应用softmax激活函数
 
        return prediction_vector

 4.4训练模型

# 定义一个函数,用于初始化训练状态
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):
    # 处理训练状态的更新
    # 包括早期停止和模型检查点
    # ...

# 定义一个函数,用于计算准确率
def compute_accuracy(y_pred, y_target):
    # 计算预测的准确率
    # ...

# 设置主要参数
args = Namespace(
    # 数据和路径信息
    surname_csv="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
)

# 检查CUDA是否可用
if not torch.cuda.is_available():
    args.cuda = False

# 设置设备
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)

# 从文件加载数据集和向量器
if args.reload_from_files:
    dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
                                                              args.vectorizer_file)
else:
    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)

# 将分类器移动到设备
classifier = 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:
    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):
            # 训练循环的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)

        # 迭代验证数据集

        # 设置:批处理生成器,设置损失和准确率到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'])

            # 步骤 3. 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.item()
            running_loss += (loss_t - running_loss) / (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']))

 4.5模型性能评估

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)  # 通过模型并应用softmax激活
 
    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: ")
# 将模型移动到CPU上,因为输入的姓氏不会是张量
classifier = classifier.to("cpu")
# 进行预测
prediction = predict_nationality(new_surname, classifier, vectorizer)
# 打印预测结果
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))
 
 
 
def predict_topk_nationality(name, classifier, vectorizer, k=5):
    """预测新姓氏的前K个最可能的国籍
    
    Args:
        name (str): 要分类的姓氏
        classifier (SurnameClassifer): 分类器的实例
        vectorizer (SurnameVectorizer): 对应的向量器
        k (int): 要返回的最可能国籍的数量
    Returns:
        一个列表,每个条目是一个字典,包含一个国籍及其概率
    """
    vectorized_name = vectorizer.vectorize(name)  # 将姓氏转换为向量表示
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)  # 调整向量大小以匹配模型输入
    prediction_vector = classifier(vectorized_name, apply_softmax=True)  # 通过模型并应用softmax激活
    probability_values, indices = torch.topk(prediction_vector, k=k)  # 获取前k个最大概率及其索引
    
    # 返回的是1,k大小的张量
    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: ")
 
# 提示用户输入要显示的预测数量
 
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']))
 
 
 
##下面就是CNN的模型预估实现代码了
##————————————————————————————————————————————————————————————————————————————————
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).unsqueeze(0)
    # 通过模型并应用softmax激活函数得到结果
    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: ")
# 将模型移动到CPU上,因为输入的姓氏不会是张量
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): 要返回的最可能国籍的数量
    Returns:
        一个列表,每个条目是一个字典,包含一个国籍及其概率
    """
    # 将输入的姓氏转换为向量表示
    vectorized_surname = vectorizer.vectorize(surname)
    # 将向量调整为适合模型输入的大小
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(dim=0)
    # 通过模型并应用softmax激活函数得到结果
    prediction_vector = classifier(vectorized_surname, apply_softmax=True)
    # 获取前k个最大概率及其索引
    probability_values, indices = torch.topk(prediction_vector, k=k)
    # 返回的是1,k大小的张量
    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

# 提示用户输入新姓氏
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)
 
print("Top {} predictions:".format(k))
print("===================")
for prediction in predictions:
    print("{} -> {} (p={:0.2f})".format(new_surname,
                                        prediction['nationality'],
                                        prediction['probability']))

  • 18
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值