《智能分类:如何用前馈神经网络识别姓氏的起源》

第一章:姓氏分类简介

姓氏分类是通过分析姓氏的字符特征,将姓氏归类到其原籍国的一种任务。这项任务在自然语言处理和机器学习领域有广泛的应用,例如人口统计信息推断、产品推荐以及公平性分析等。姓氏分类的挑战在于姓氏的多样性和复杂性,不同国家和地区的姓氏具有不同的字符模式和长度。通过使用神经网络模型,例如多层感知器(MLP)和卷积神经网络(CNN),我们可以自动化地进行姓氏分类,提高分类的准确性和效率。

第二章:前馈神经网络技术概述

前馈神经网络是一类没有反馈连接的神经网络,信息只在一个方向上传递。它们是深度学习的基础组件,广泛应用于各种任务中,如图像分类、语音识别和自然语言处理。本文将详细介绍两种主要的前馈神经网络:多层感知器(MLP)和卷积神经网络(CNN)。

2.1 多层感知器(MLP)

感知机是最简单的神经网络模型,由输入层、输出层和一个权重向量组成。感知机的基本原理是将输入向量与权重向量进行内积运算,然后通过激活函数得到输出。感知机只能处理线性可分的任务,对于非线性问题则无能为力。

2.1.1 感知机基础

感知机是最基本的神经网络单元,能够对线性可分的数据进行分类。感知机的基本结构包括输入层、权重、偏置和激活函数。输入层接收特征向量,权重和偏置用于线性变换,激活函数引入非线性变换。
输入层: 接收特征向量,表示为 x=[x1,x2,x3,…]。
权重和偏置: 每个输入特征都有一个对应的权重wi ,以及一个偏置bi 。
线性变换: 计算线性组合 z=wixi+bi。
激活函数: 将线性组合通过激活函数 f(z)转换为非线性输出。常用的激活函数包括Sigmoid、Tanh和ReLU。
在这里插入图片描述
常见的激活函数:在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
感知机和人脑神经元在结构和功能上有一些相似之处,它们的复杂性和具体的工作机制存在显著差异。感知机是对人脑神经元的一个高度简化和抽象化的模型,用于理解和模拟一些基本的神经计算功能
在这里插入图片描述

2.1.2 多层感知机架构

多层感知器由多个感知器层组成,每一层包括多个神经元。输入层接收原始数据,经过一个或多个隐藏层的非线性变换,最终输出层给出分类结果。隐藏层的非线性激活函数(如ReLU)是MLP能够处理非线性问题的关键。
输入层: 接收输入数据并传递到第一个隐藏层。
隐藏层: 每个隐藏层由多个神经元组成,每个神经元接收前一层的输出,进行线性变换和非线性激活。
输出层: 最后一层,通常是一个全连接层,输出分类或回归结果。
MLP的训练过程通过反向传播算法进行,最小化损失函数(如交叉熵或均方误差),调整网络的权重和偏置。
在这里插入图片描述

2.2 卷积神经网络(CNN)详解

卷积神经网络(CNN)是一种专门用于处理具有网格拓扑结构数据(如图像、语音)的神经网络。CNN通过卷积层、池化层和全连接层的组合,能够有效地提取数据的局部特征。
在这里插入图片描述
该算法流程示例:首先从输入层接收28x28的图像数据。接着,图像经过两个卷积层Conv_1和Conv_2,每个卷积层都有5个大小为5x5的滤波器。在每个卷积层之后,图像通过一个Max-Pooling操作进行下采样,将图像的大小减半。

然后,图像进入全连接层,全连接层的节点数根据具体任务的需求而变化。在此之后,使用ReLU激活函数增强网络的非线性表达能力。为了防止过拟合,还添加了一个Dropout层,随机关闭一些神经元。

最后,图像到达输出层,输出层的节点数取决于具体的任务目标。例如,对于分类任务,每个节点可能表示一个类别的概率。

2.2.1 基于卷积层的深度学习
2.2.1.1 理解卷积核

卷积核(也称为滤波器)是CNN的核心组件。卷积核是一个小尺寸的权重矩阵,通过在输入数据上滑动进行卷积运算,提取局部特征。卷积核的大小和数量是影响CNN性能的重要因素。
在这里插入图片描述

2.2.1.2 探究卷积步长

步长的定义: 步长(Stride)是指卷积核在输入数据上滑动的步伐大小。步长为1表示每次滑动一个像素,步长为2表示每次滑动两个像素。
步长对卷积结果的影响: 步长越大,输出特征图的尺寸越小,计算量也随之减少;步长越小,输出特征图的尺寸越大,但计算量增加。
通俗理解: 如果你在阅读一本书(输入数据),每次翻一页(步长为1),你会看到更多的细节;如果每次翻两页(步长为2),也许会更快读完,但可能会错过一些细节。

2.2.1.3 卷积运算维数及操作

卷积运算可以在不同维度上进行,例如一维卷积、二维卷积和三维卷积。
一维卷积主要用于处理序列数据,如时间序列或文本数据。卷积核在一维数据上滑动,提取序列中的局部特征。
二维卷积常用于图像处理,卷积核在二维平面上滑动,提取图像中的局部特征,如边缘、角点等。
三维卷积用于处理三维数据,如视频数据,其中卷积核在三维空间内滑动,提取视频帧中的时空特征。
**通俗例子:**一维卷积像是沿着一条直线(如文本行)滑动,二维卷积像是在平面(如照片)上滑动,三维卷积像是在立体空间(如视频帧)中滑动。

2.2.1.4 通道在CNN中的角色

通道(也称为特征图)是CNN中的一个重要概念。输入数据可以有多个通道,例如彩色图像有RGB三个通道。卷积层的输出也是多个通道,每个通道对应一个卷积核的输出。通道的数量和组合方式对CNN的特征提取能力有重要影响。

2.2.1.5 核大小的影响分析

卷积核的大小直接影响特征提取的范围和效果。较小的卷积核(如3x3)可以捕捉细节特征,但需要更多层次来捕捉大范围特征;较大的卷积核(如5x5或7x7)可以捕捉更大范围的特征,但计算量更大。

2.2.1.6 边界填充理论

边界填充(Padding)是指在输入数据的边界添加额外的像素,以保持卷积运算后特征图的尺寸不变。常见的填充方式有零填充和镜像填充。边界填充可以有效地保留输入数据的边界信息。

2.2.2 激活层的应用

激活层是CNN中的非线性变换层,常用的激活函数有ReLU、Sigmoid和Tanh。激活层的作用是引入非线性,使得网络可以处理复杂的非线性问题。

2.2.3 池化层的作用

池化层(Pooling Layer)是CNN中的降维层,通过对特征图进行下采样,减少数据的维度和计算量。常用的池化操作有最大池化(Max Pooling)和平均池化(Average Pooling),可以有效地提取特征的空间不变性。
在这里插入图片描述

2.2.4 全连接层的重要性

全连接层(Fully Connected Layer)是CNN中的最后几层,其作用是将提取到的特征进行整合和分类。全连接层通过与所有神经元的连接,能够有效地学习和表达复杂的特征关系。通过全连接层,网络可以学习到不同特征之间的相互关系,从而实现更加复杂的模式识别和分类任务。
通俗理解: 全连接层像是一个投票系统,综合考量每个人(神经元)的投票结果来决定最终的结果(分类)。

第三章:姓氏分类模型实现

3.1 任务目标与意义

姓氏分类的任务目标是根据给定的姓氏预测其所属的类别,例如国家、地区或族群。这一任务的意义在于通过对姓氏的分类,可以从中提取出丰富的文化、地理和历史信息,为人口统计分析、文化研究和遗传学研究提供重要的数据支持。

3.2 开发环境配置

本次项目是在Jupyter Notebook上进行

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 实现数据集构建

姓氏分类的数据集通常包括大量的姓氏及其对应的类别标签。数据集的构建包括数据收集、数据清洗和数据标注等步骤。以下是一个典型的数据集构建流程:
数据收集:从公开数据源或自有数据源中收集姓氏数据
数据清洗:去除重复、错误和无效的数据
数据标注:为每个姓氏添加类别标签

4.2 深入多层感知器模型构建

4.2.1 数据向量化处理
4.2.1.1 构建词汇表
class Vocabulary(object):
    """处理文本并提取用于映射的词汇表的类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        初始化函数
        参数:
            token_to_idx (dict): 预先存在的token到索引的映射字典
            add_unk (bool): 是否添加UNK(未知)token的标志
            unk_token (str): 要添加到词汇表中的UNK token
        """

        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):
        """从一个序列化的字典实例化Vocabulary对象"""
        return cls(**contents)

    def add_token(self, token):
        """基于token更新映射字典

        参数:
            token (str): 要添加到词汇表中的项
        返回:
            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):
        """将一个token列表添加到词汇表中
        
        参数:
            tokens (list): 一个字符串token列表
        返回:
            indices (list): 对应于这些token的索引列表
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """检索与token关联的索引,如果token不存在则返回UNK索引
        
        参数:
            token (str): 要查找的token
        返回:
            index (int): 对应于该token的索引
        备注:
            如果要实现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
        
        参数: 
            index (int): 要查找的索引
        返回:
            token (str): 对应于该索引的token
        异常:
            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)
4.2.1.2 向量化技术

姓氏向量化器类:用于协调词汇表并将其应用于姓氏向量化


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

    def __init__(self, surname_vocab, nationality_vocab):
        """
        初始化函数
        参数:
            surname_vocab (Vocabulary): 将字符映射到整数的词汇表
            nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
        """
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab
 
    def vectorize(self, surname):
        """
        将姓氏向量化为独热编码
        参数:
            surname (str): 姓氏
        返回:
            one_hot (np.ndarray): 压缩的独热编码
        """
        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):
        """从数据集数据框实例化向量化器
        
        参数:
            surname_df (pandas.DataFrame): 姓氏数据集
        返回:
            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.3 数据集处理

姓氏数据集类:用于加载和处理姓氏数据的PyTorch数据集


class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        初始化函数
        参数:
            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):
        """加载数据集并从头创建一个新的向量化器
        
        参数:
            surname_csv (str): 数据集的位置
        返回:
            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):
        """加载数据集和相应的向量化器。用于向量化器已被缓存以供重复使用的情况
        
        参数:
            surname_csv (str): 数据集的位置
            vectorizer_filepath (str): 保存的向量化器的位置
        返回:
            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):
        """从文件加载向量化器的静态方法
        
        参数:
            vectorizer_filepath (str): 序列化向量化器的位置
        返回:
            SurnameVectorizer的一个实例
        """
        with open(vectorizer_filepath) as fp:
            return SurnameVectorizer.from_serializable(json.load(fp))
 
    def save_vectorizer(self, vectorizer_filepath):
        """使用json将向量化器保存到磁盘
        
        参数:
            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数据集的主要入口方法
        
        参数:
            index (int): 数据点的索引 
        返回:
            一个包含数据点特征(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):
        """给定批大小,返回数据集中的批次数量
        
        参数:
            batch_size (int)
        返回:
            数据集中的批次数量
        """
        return len(self) // batch_size
 
    
def generate_batches(dataset, batch_size, shuffle=True, drop_last=True, device="cpu"):
    """
    一个包装了PyTorch DataLoader的生成器函数。它将确保每个张量都位于正确的设备位置。
    
    参数:
        dataset (Dataset): 输入的数据集
        batch_size (int): 每批次的数据量
        shuffle (bool): 是否对数据进行随机打乱
        drop_last (bool): 是否丢弃最后一个不完整的批次
        device (str): 设备类型,如 "cpu" 或 "cuda"
    产出:
        生成的数据字典,每个字典包含批次的数据和标签
    """
    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.2 构建多层感知器:SurnameClassifier

姓氏分类器类:用于姓氏分类的两层多层感知器


class SurnameClassifier(nn.Module):
    """ 用于姓氏分类的两层多层感知器 """
    
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        初始化函数
        参数:
            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):
        """分类器的前向传递过程
        
        参数:
            x_in (torch.Tensor): 输入数据张量
                x_in的形状应为 (batch, input_dim)
            apply_softmax (bool): 是否应用softmax激活的标志
                如果与交叉熵损失一起使用,应该为False
        返回:
            结果张量. 张量形状应为 (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.3 卷积神经网络模型构建

4.3.1 数据向量化处理细节
4.3.1.1 词汇表建立
class Vocabulary(object):
    """处理文本并提取用于映射的词汇表的类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        初始化函数
        参数:
            token_to_idx (dict): 预先存在的token到索引的映射字典
            add_unk (bool): 是否添加UNK(未知)token的标志
            unk_token (str): 要添加到词汇表中的UNK token
        """

        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):
        """从一个序列化的字典实例化Vocabulary对象"""
        return cls(**contents)

    def add_token(self, token):
        """基于token更新映射字典

        参数:
            token (str): 要添加到词汇表中的项
        返回:
            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):
        """将一个token列表添加到词汇表中
        
        参数:
            tokens (list): 一个字符串token列表
        返回:
            indices (list): 对应于这些token的索引列表
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """检索与token关联的索引,如果token不存在则返回UNK索引
        
        参数:
            token (str): 要查找的token
        返回:
            index (int): 对应于该token的索引
        备注:
            如果要实现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
        
        参数: 
            index (int): 要查找的索引
        返回:
            token (str): 对应于该索引的token
        异常:
            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)
4.3.1.2 向量化策略

姓氏向量化器类:协调词汇表并将其应用于姓氏向量化


class SurnameVectorizer(object):
    """ 协调词汇表并将其投入使用的向量化器 """
    
    def __init__(self, surname_vocab, nationality_vocab, max_surname_length):
        """
        初始化函数
        参数:
            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):
        """
        将姓氏向量化为独热编码矩阵
        参数:
            surname (str): 姓氏
        返回:
            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):
        """从数据集数据框实例化向量化器
        
        参数:
            surname_df (pandas.DataFrame): 姓氏数据集
        返回:
            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.3 整理数据集

姓氏数据集类:用于加载和处理姓氏数据的PyTorch数据集



class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        初始化函数
        参数:
            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):
        """加载数据集并从头创建一个新的向量化器
        
        参数:
            surname_csv (str): 数据集的位置
        返回:
            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):
        """加载数据集和相应的向量化器。用于向量化器已被缓存以供重复使用的情况
        
        参数:
            surname_csv (str): 数据集的位置
            vectorizer_filepath (str): 保存的向量化器的位置
        返回:
            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):
        """从文件加载向量化器的静态方法
        
        参数:
            vectorizer_filepath (str): 序列化向量化器的位置
        返回:
            SurnameVectorizer的一个实例
        """
        with open(vectorizer_filepath) as fp:
            return SurnameVectorizer.from_serializable(json.load(fp))
 
    def save_vectorizer(self, vectorizer_filepath):
        """使用json将向量化器保存到磁盘
        
        参数:
            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数据集的主要入口方法
        
        参数:
            index (int): 数据点的索引 
        返回:
            一个包含数据点特征(x_data)和标签(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):
        """给定批大小,返回数据集中的批次数量
        
        参数:
            batch_size (int)
        返回:
            数据集中的批次数量
        """
        return len(self) // batch_size
 
    
def generate_batches(dataset, batch_size, shuffle=True, drop_last=True, device="cpu"):
    """
    一个包装了PyTorch DataLoader的生成器函数。它将确保每个张量都位于正确的设备位置。
    参数:
        dataset (Dataset): 输入的数据集
        batch_size (int): 每批次的数据量
        shuffle (bool): 是否对数据进行随机打乱
        drop_last (bool): 是否丢弃最后一个不完整的批次
        device (str): 设备类型,如 "cpu" 或 "cuda"
    产出:
        生成的数据字典,每个字典包含批次的数据和标签
    """
    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.2 构建CNN模型:SurnameClassifier

姓氏分类器类:用于姓氏分类的卷积神经网络


class SurnameClassifier(nn.Module):
    def __init__(self, initial_num_channels, num_classes, num_channels):
        """
        初始化函数
        参数:
            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):
        """分类器的前向传递过程
        
        参数:
            x_surname (torch.Tensor): 输入数据张量
                x_surname的形状应为 (batch, initial_num_channels, max_surname_length)
            apply_softmax (bool): 是否应用softmax激活的标志
                如果与交叉熵损失一起使用,应该为False
        返回:
            结果张量. 张量形状应为 (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 模型训练流程

计算准确率的函数 compute_accuracy

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

训练状态的管理函数

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

主代码块

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
)
 
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 not torch.cuda.is_available():
    args.cuda = False
 
args.device = torch.device("cuda" if args.cuda else "cpu")
print("Using 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:
    # 从检查点开始训练
    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):
            # 训练步骤:
 
            # --------------------------------------
            # 第一步: 清零梯度
            optimizer.zero_grad()
 
            # 第二步: 计算输出
            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)
 
            # 第四步: 使用损失反向传播梯度
            loss.backward()
 
            # 第五步: 使用优化器更新参数
            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.0
        running_acc = 0.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)
            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.0
running_acc = 0.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']))

4.5 对模型性能的评估与分析

MLP:

MLP(多层感知器)模型性能分析
训练和验证过程

在训练和验证过程中,首先采用多层感知器(MLP)模型来对姓氏进行分类。以下是模型的各项性能指标和结果分析:
训练损失和准确率: 在训练过程中,模型的损失逐渐降低,准确率逐渐提高,这表明模型在学习数据的特征。
验证损失和准确率: 验证集上的损失和准确率也表现出了类似的趋势,但验证损失在某些迭代中可能会有所波动,这可能是由于模型在验证集上遇到了一些难以分类的样本。在这里插入图片描述
测试集性能
在训练结束后,我们在测试集上评估了模型的性能:
Test loss: 1.819154896736145;
Test Accuracy: 46.68749999999999

在这里插入图片描述

推理结果
在这里插入图片描述

通过推理函数 predict_nationality 和 predict_topk_nationality,我们可以预测新的姓氏的国籍及其概率。例如,输入姓氏 “wen”,模型预测其国籍为 “Chinese”,概率为 0.45。
推理结果示例:
wen -> Chinese (p=0.45)
wen -> English (p=0.20)
wen -> Dutch (p=0.14)
wen -> German (p=0.08)
wen -> Korean (p=0.07)

CNN:

训练和验证过程
进一步使用卷积神经网络(CNN)模型来进行姓氏分类。以下是模型的各项性能指标和结果分析:
训练损失和准确率:在训练过程中,CNN模型的损失和准确率表现出良好的收敛性。
验证损失和准确率:验证集上的损失和准确率也表现出良好的趋势,表明模型在验证集上的表现较好。
测试集性能
在训练结束后,模型在测试集上的损失为 1.9216371824343998,准确率为 60.7421875%。这表明CNN模型在测试集上的表现优于MLP模型。在这里插入图片描述

推理结果

经过推理函数 predict_nationality 和 predict_topk_nationality,预测新的姓氏的国籍及其概率。例如,输入姓氏 “wen”,模型预测其国籍为 “Chinese”,概率为 0.77。
模型预测姓氏 “wen” 的前5个可能国籍及其概率分别为:
wen -> Chinese (p=0.77)
wen -> Korean (p=0.18)
wen -> English (p=0.02)
wen -> Dutch (p=0.01)
wen -> German (p=0.01)

在这里插入图片描述

总结

通过对MLP和CNN模型的训练和测试,我们发现CNN模型在测试集上的表现优于MLP模型。尽管如此,两种模型在推理阶段都能给出合理的预测结果。未来可以通过进一步优化模型结构和超参数,提升模型的性能和准确率。

改进方向

1.增加数据集规模和多样性:收集更多不同国籍的姓氏数据,以提高模型的泛化能力。
2.数据增强:使用数据增强技术来平衡数据集,减少模型偏向某些国籍的情况。
3.超参数调整:尝试不同的模型架构和超参数设置,以找到最优的模型配置。
4.正则化技术:使用正则化技术(如Dropout、L2正则化)来防止过拟合。
5.模型简化:简化模型结构,减少过拟合的风险。

第五章:未来实验探索的方向

5.1 引入自然语言处理技术

在姓氏分类任务中引入更多的自然语言处理技术,如词嵌入(Word Embedding)、序列到序列模型(Seq2Seq)等,可以进一步提高模型的性能。

5.2 GANs在姓氏生成中的应用

生成对抗网络(GANs)可以用于生成新的姓氏,通过生成模型和判别模型的对抗训练,生成具有特定风格和特征的姓氏。

5.3 深度学习模型的可解释性

研究深度学习模型的可解释性,理解模型的决策过程对于提高模型的透明度和可信度具有重要意义。可以通过以下几种方法来增强模型的可解释性:
1.可视化技术:使用可视化工具展示模型的中间层输出和特征图,帮助理解模型如何处理输入数据。
2.特征重要性分析:评估输入特征对模型预测结果的影响,识别关键特征。
3.模型简化:使用简化的模型结构,如决策树或线性模型,作为复杂深度学习模型的近似解释器。

5.4 跨文化姓氏分类挑战

跨文化姓氏分类面临着许多挑战,例如不同文化背景下姓氏的多样性和复杂性。为了应对这些挑战,可以考虑以下策略:
多语言数据集:构建包含多种语言和文化背景的姓氏数据集,提高模型的泛化能力。
迁移学习:利用在一个文化背景下训练的模型参数,迁移到其他文化背景的姓氏分类任务中。
混合模型:结合多种模型结构,如CNN和RNN,处理不同文化背景下的姓氏特征。

5.5 大数据技术在模型训练中的运用

大数据技术在姓氏分类模型的训练中具有重要作用,可以通过以下几种方式提高模型的训练效率和性能:
分布式计算:使用分布式计算框架(如Apache Spark、Hadoop)处理大规模姓氏数据集,加速数据预处理和模型训练过程。
云计算平台:利用云计算平台(如AWS、Google Cloud、Azure)提供的高性能计算资源,进行大规模模型训练和评估。
数据增强:通过数据增强技术(如数据扩充、噪声注入)增加训练数据的多样性,提高模型的鲁棒性。

第六章:实验总结

在本实验中,我们详细探讨了姓氏分类任务及其在自然语言处理中的应用。通过构建和训练多层感知器(MLP)和卷积神经网络(CNN)两种前馈神经网络模型,实现了对姓氏的分类。实验结果表明,深度学习模型在处理复杂的非线性问题和提取数据特征方面具有显著优势。
在未来的研究中,可以进一步探索引入更多自然语言处理技术、生成对抗网络(GANs)以及增强模型可解释性的方法。此外,跨文化姓氏分类和大数据技术的应用也是值得深入研究的方向。

通过这篇博客文章,我们深入探讨了姓氏分类与前馈神经网络的研究,涵盖了从理论到实践的各个方面。希望这篇文章能够为您提供有价值的参考,帮助您更好地理解和应用深度学习技术。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值