CNN姓氏国籍分类pytorch

近年来,随着全球化的加速和人口流动的增加,对于人们来说,了解他人的姓氏和国籍已经变得越来越重要。姓氏国籍分类是一项重要的任务,可以帮助人们更好地理解和尊重不同文化和背景的个体。然而,由于姓氏的多样性和复杂性,传统的人工分类方法已经变得困难和耗时。因此,利用深度学习技术,特别是卷积神经网络(CNN),来实现姓氏国籍分类已成为一个备受关注和研究的领域。本文将介绍CNN在姓氏国籍分类方面的应用,以及它在解决这一问题上的优势和挑战。


前言

NLP技术中对于RNN和transformer的使用广为人知,其实CNN在NLP中也有较多应用。本文并未使用MLP等网络进行,而是利用CNN网络,细致地一步步实现姓氏国籍分类。本文将介绍CNN在姓氏国籍分类方面的应用,以及它在解决这一问题上的优势和挑战。


一、CNN是什么?

CNN的介绍在网上已经很多了,我就不搬运了,可以点击这个链接(二维卷积)去看大佬的博客。本代码使用的是一维卷积conv1d,也可以点击这个链接去了解一下。如果有人需要补充信号处理的知识,请自行搜索。

二、实现步骤

目录

文章目录

前言

一、CNN是什么?

二、实现步骤

1.引入库

2.定义数据集

总结


1.引入库

代码如下:

版本应该没什么太多要求,随便找一个之前跑通的环境下几个包应该就行了。

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

2.定义数据集

       定义了一个 Vocabulary 类,用于处理文本并提取词汇进行映射。主要用于将文本数据转化为数值表示,以便于机器学习模型处理。它能够将标记(如单词或字符)映射到索引,并提供相应的查找功能。它还支持处理未知标记,通过将其映射到预定义的UNK标记索引。

       这里是这个类的详细解释:

__init__ 方法
  • 功能:初始化词汇表对象。
  • 参数
    • token_to_idx:一个将标记映射到索引的字典,默认为空字典。
    • add_unk:一个布尔值,指示是否添加一个用于未知标记的UNK标记,默认为 True
    • unk_token:要添加到词汇表中的UNK标记,默认为 "<UNK>"
to_serializable 方法
  • 功能:将词汇表对象转换为一个可序列化的字典。
from_serializable 类方法
  • 功能:从一个序列化的字典实例化一个词汇表对象。
add_token 方法
  • 功能:将一个新的标记添加到词汇表中,并返回其索引。
add_many 方法
  • 功能:将多个标记添加到词汇表中,并返回这些标记对应的索引列表。
lookup_token 方法
  • 功能:返回给定标记的索引,如果标记不存在且已添加UNK标记,则返回UNK标记的索引。
lookup_index 方法
  • 功能:返回给定索引对应的标记,如果索引不在词汇表中则抛出 KeyError 异常。
__str__ 方法
  • 功能:返回词汇表对象的字符串表示,显示词汇表的大小。
__len__ 方法
  • 功能:返回词汇表中标记的数量。
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)

3.姓氏向量化

       这个 SurnameVectorizer 类主要用于将姓氏数据转化为数值表示,以便于机器学习模型处理。它能够将姓氏中的每个字符转化为one-hot向量,并根据姓氏数据集构建相应的词汇表。它还支持将对象保存为可序列化的格式,以便于存储和加载。

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
        }
__init__ 方法
  • 功能:初始化向量化器对象。
  • 参数
    • surname_vocab:用于将字符映射到整数的词汇表对象。
    • nationality_vocab:用于将国籍映射到整数的词汇表对象。
    • max_surname_length:最长姓氏的长度,用于确定独热向量矩阵的大小。
vectorize 方法
  • 功能:将姓氏转化为独热向量矩阵。
  • 参数
    • surname:需要转化的姓氏字符串。
  • 返回值:一个独热向量矩阵,表示姓氏中每个字符在词汇表中的位置。
from_dataframe 类方法
  • 功能:从一个包含姓氏数据的数据框实例化一个向量化器对象。
  • 参数
    • surname_df:包含姓氏和国籍的数据框。
  • 返回值:一个 SurnameVectorizer 实例。
from_serializable 类方法
  • 功能:从一个可序列化的字典实例化一个向量化器对象。
  • 参数
    • contents:一个包含向量化器属性的字典。
  • 返回值:一个 SurnameVectorizer 实例。
to_serializable 方法
  • 返回值:一个包含向量化器属性的字典。

4.定义姓氏数据集类

        定义了一个 SurnameDataset 类,用于处理姓氏数据并将其转换为机器学习模型可以处理的格式。该类继承自 PyTorch 的 Dataset 类,并且包含了一些有用的方法来加载、保存和处理数据。它支持从文件加载数据集和向量化器,保存向量化器,并将数据分割为训练、验证和测试集。生成批次的函数则用于在训练过程中批量获取数据,确保数据在正确的设备上。

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
__init__ 方法
  • 功能:初始化数据集对象,并根据数据集分割(训练、验证、测试)进行相应的处理。
  • 参数
    • surname_df:包含姓氏数据的数据框。
    • vectorizer:用于将数据转化为向量的向量化器对象。
  • 主要步骤
    • 根据数据集的分割(训练、验证、测试)创建不同的数据集。
    • 计算每个类别的权重,用于处理不平衡数据。
load_dataset_and_make_vectorizer 类方法
  • 功能:加载数据集并从头创建一个新的向量化器。
  • 参数
    • surname_csv:数据集文件的位置。
  • 返回值:一个 SurnameDataset 实例。
load_dataset_and_load_vectorizer 类方法
  • 功能:加载数据集并加载对应的向量化器。
  • 参数
    • surname_csv:数据集文件的位置。
    • vectorizer_filepath:已保存的向量化器文件的位置。
  • 返回值:一个 SurnameDataset 实例。
load_vectorizer_only 静态方法
  • 功能:从文件加载向量化器。
  • 参数
    • vectorizer_filepath:序列化向量化器文件的位置。
  • 返回值:一个 SurnameVectorizer 实例。
save_vectorizer 方法
  • 功能:将向量化器保存到磁盘。
  • 参数
    • vectorizer_filepath:保存向量化器文件的位置。
get_vectorizer 方法
  • 功能:返回向量化器对象。
set_split 方法
  • 功能:选择数据集的分割(训练、验证、测试)。
  • 参数
    • split:数据集分割类型,默认为 "train"。
__len__ 方法
  • 功能:返回当前分割的数据集大小。
__getitem__ 方法
  • 功能:返回数据点的特征和标签。
  • 参数
    • index:数据点的索引。
  • 返回值:一个包含数据点特征和标签的字典。
get_num_batches 方法
  • 功能:返回数据集中的批次数量。
  • 参数
    • batch_size:批次大小。
  • 返回值:数据集中的批次数量。
generate_batches方法
  • 功能:生成批次数据,确保每个张量在正确的设备上。
  • 参数
    • dataset:数据集对象。
    • batch_size:批次大小。
    • shuffle:是否打乱数据,默认为 True
    • drop_last:是否丢弃最后一个不足批次大小的批次,默认为 True
    • device:设备类型(如 "cpu" 或 "cuda")。

5.分类器定义

       定义了一个 SurnameClassifier 类,使用卷积神经网络 (CNN) 对姓氏进行分类。这个 SurnameClassifier 类主要用于将姓氏数据输入模型,进行卷积处理,并输出预测的国籍类别。该类使用了卷积神经网络来提取姓氏的特征,并通过全连接层输出预测结果。它可以选择性地对输出应用 softmax 激活,以便于后续计算交叉熵损失或直接获取概率分布。

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
__init__ 方法
  • 功能:初始化分类器对象,定义卷积神经网络的层次结构。

  • 参数

    • initial_num_channels:输入特征向量的大小(即输入通道数)。
    • num_classes:输出预测向量的大小(即类别数)。
    • num_channels:卷积网络中使用的通道大小(即特征图的通道数)。
  • 主要组件

    • self.convnet:一个由 nn.Sequential 组成的卷积神经网络,包含四个卷积层,每个卷积层后面接一个 ELU 激活函数。
    • self.fc:一个全连接层,用于输出最终的预测结果。
forward 方法
  • 功能:定义分类器的前向传播过程,将输入数据通过卷积网络和全连接层,输出预测结果。

  • 参数

    • x_surname:输入数据张量,其形状应为 (batch, initial_num_channels, max_surname_length)
    • apply_softmax:一个布尔标志,指示是否对输出应用 softmax 激活函数。
  • 返回值:结果张量,其形状应为 (batch, num_classes)

  • 主要步骤

    • features = self.convnet(x_surname).squeeze(dim=2):将输入数据通过卷积网络,得到特征张量,并在第2维上进行压缩。
    • prediction_vector = self.fc(features):将特征张量输入全连接层,得到预测向量。
    • if apply_softmax::如果 apply_softmaxTrue,则对预测向量进行 softmax 激活。

6.定义训练所用函数

make_train_state

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
    }
  • 功能:初始化一个训练状态字典,用于跟踪训练过程中的重要信息。
  • 参数
    • args:包含训练参数的对象或字典,例如学习率和模型文件名。
  • 返回值:包含训练状态的字典,包括提前停止标志、最佳验证损失、当前学习率、当前 epoch 索引、训练和验证的损失和准确率、测试损失和准确率,以及模型文件名。

update_train_state

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:包含训练参数的对象或字典,例如提前停止标准。
    • model:正在训练的模型。
    • train_state:代表训练状态的字典。
  • 主要步骤
    • 如果是第一个 epoch,保存模型状态并设置 stop_earlyFalse
    • 如果是后续的 epoch,比较当前验证损失和之前的验证损失:
      • 如果当前损失不比之前好,增加提前停止步数。
      • 如果当前损失有所改善,保存模型状态并重置提前停止步数。
    • 检查是否需要提前停止训练。

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
  • 功能:计算模型的准确率。
  • 参数
    • y_pred:模型的预测输出张量。
    • y_target:真实的标签张量。
  • 主要步骤
    • 获取预测张量中最大值的索引,即预测的类别。
    • 计算预测正确的样本数。
    • 返回准确率,即正确预测的样本数占总样本数的百分比。

7.训练及测试过程

参数配置

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("扩展后的文件路径: ")
    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("使用 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']))

        按上述超参数训练100epoch后,结果勉强能用,后续可进行调参(炼丹得到更好效果)

8.预测指定姓氏国籍

predict_nationality函数

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}
  • 功能:预测给定姓氏的国籍。
  • 参数
    • surname:要分类的姓氏字符串。
    • classifier:分类器实例。
    • vectorizer:向量化器实例。
  • 步骤
    • 将姓氏向量化。
    • 将向量转换为张量并增加一个维度以匹配模型的输入格式。
    • 使用分类器进行预测,并应用 softmax 激活函数。
    • 找到最大概率值及其对应的索引。
    • 查找并返回预测的国籍及其概率。

示例

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']))

(咱也不知道对不对)

predict_topk_nationality函数

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
  • 功能:预测给定姓氏的前 K 个可能的国籍。
  • 参数
    • surname:要分类的姓氏字符串。
    • classifier:分类器实例。
    • vectorizer:向量化器实例。
    • k:要返回的前 K 个国籍数。
  • 步骤
    • 将姓氏向量化。
    • 将向量转换为张量并增加一个维度以匹配模型的输入格式。
    • 使用分类器进行预测,并应用 softmax 激活函数。
    • 找到前 K 个最大概率值及其对应的索引。
    • 查找并返回前 K 个预测的国籍及其概率。

示例

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']))

三、CNN在本问题中的应用

应用过程

  1. 输入处理

           输入的姓氏首先通过 vectorizer 被转换为一个独热向量矩阵。矩阵的大小为 (字符集大小, 姓氏最大长度),其中每一列表示一个字符的独热向量。
  2. 卷积层

           卷积层通过滑动窗口对输入矩阵进行局部特征提取。每个卷积核在输入矩阵上滑动,提取局部模式,如字符的组合模式或子串模式。在代码中,有四个卷积层,每层有不同的卷积核数目和滑动步长,逐层提取越来越抽象的特征:
    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()
    )
    

  3. 激活函数

           在每个卷积层后,使用了 ELU(指数线性单元)激活函数,引入非线性,使模型可以学习到更复杂的模式。
  4. 全连接层

            最后一层卷积层的输出通过 self.fc 全连接层映射到最终的国籍分类。全连接层将卷积层提取到的特征映射到不同的国籍类别。
    self.fc = nn.Linear(num_channels, num_classes)  # 全连接层,用于输出最终的预测结果
    

  5. Softmax 激活

           在输出层,使用 Softmax 激活函数,将输出转换为概率分布,用于确定每个国籍类别的概率:
    if apply_softmax:
        prediction_vector = F.softmax(prediction_vector, dim=1)
    

优势

  1. 局部特征提取:CNN 擅长提取局部特征,可以自动学习姓氏中字符的组合模式和结构特征,而不需要人工设计特征。
  2. 参数共享和稀疏连接:卷积核在输入矩阵上滑动,同一个卷积核在不同位置共享参数,这显著减少了参数数量,提高了训练效率和泛化能力。
  3. 层次化特征表示:多层卷积层可以提取越来越抽象的特征,从字符级别到子串级别,最终形成高层次的特征表示,有助于更准确地分类。

弊端

  1. 需要大量数据:CNN 模型通常需要大量标注数据进行训练,以充分学习到有效的特征。如果训练数据不足,模型容易过拟合。
  2. 计算复杂度高:尤其是对于深层网络,卷积操作和反向传播计算复杂度较高,需要较强的计算资源和较长的训练时间。
  3. 特征解释性差:CNN 自动提取特征,难以解释中间层次的特征表示具体代表什么。对于某些需要高解释性的应用场景,可能不够透明。
  4. 对变形和旋转不敏感
    • 虽然 CNN 能够很好地处理局部特征,但对输入数据的变形和旋转敏感度较低。在某些应用中,可能需要额外的数据增强或特定设计来提升模型的鲁棒性。

小总结

       在姓氏国籍分类问题中,CNN 能够高效地提取局部字符模式,适应不同长度的姓氏,并利用共享参数和层次化特征表示提升分类性能。然而,模型的高计算复杂度和对大量训练数据的依赖也是需要考虑的问题。在实际应用中,可能需要平衡这些优势和弊端,根据具体需求进行模型选择和优化。

       在MLP的国籍分类任务中,同样是100epoch,效果不如CNN:


总结

在这篇博客中,我们深入探讨了如何利用卷积神经网络(CNN)进行姓氏国籍分类。这一任务不仅展示了深度学习在文本分类中的强大能力,同时也强调了数据预处理和模型选择的重要性。

数据预处理

我们从数据预处理开始,将姓氏转换为独热向量矩阵,这使得每个姓氏都能被表示为固定大小的矩阵。利用 PyTorch 的 DatasetDataLoader 类,我们有效地管理和批量处理数据,为后续的模型训练打下了坚实的基础。

模型构建

核心部分是构建 CNN 模型 SurnameClassifier。通过四个卷积层和一个全连接层,模型能够逐层提取和压缩姓氏中的局部和全局特征。卷积层使用共享参数的方式高效地提取特征,并通过 ELU 激活函数引入非线性,使模型能够捕捉到更加复杂的模式。

训练过程

在训练过程中,我们采用了交叉熵损失函数和 Adam 优化器,并结合提前停止和学习率调度策略,防止过拟合并优化训练效率。通过进度条实时监控训练和验证的损失与准确率,我们能够及时调整训练策略,确保模型性能的逐步提升。

预测功能

为了实际应用,我们实现了 predict_nationalitypredict_topk_nationality 函数,可以预测新姓氏的国籍及其概率。通过这些函数,用户可以方便地输入姓氏并获取预测结果,体验深度学习模型的强大功能。

优势与挑战

我们讨论了 CNN 在姓氏分类中的优势,如高效的局部特征提取和参数共享,以及其需要大量数据和高计算资源等挑战。理解这些优势和挑战,有助于我们在实际应用中更好地选择和优化模型。

维度变化

通过分析输入数据的形状和每层 CNN 的维度变化,我们更深入地理解了模型的工作原理。每一层卷积操作都逐步提取和压缩特征,最终通过全连接层输出预测结果。这种结构使得模型能够高效地学习和分类复杂的姓氏特征。

总结

这篇博客全面展示了如何使用 CNN 进行姓氏国籍分类,从数据预处理、模型构建到训练和预测。通过实际的代码实例和详细的解释,读者可以掌握如何在实际项目中应用 CNN 进行文本分类。我们希望这篇博客不仅能帮助读者理解 CNN 的强大功能,还能激发他们在更多领域探索和应用深度学习技术。

(本文格式可能有一些问题,后续可能修改)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值