使用前馈神经网络和卷积神经网络实现姓氏分类

一、环境配置

python 3.7

torch==2.1.0

二、数据预处理

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

2.1 关于MLP的数据预处理

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),  # 查找字典

        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)  # 读取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)  # 读取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。
    它将确保每个张量在正确的设备位置上。
    """
    dataloader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last)  # 创建DataLoader
 
    for data_dict in dataloader:
        out_data_dict = {}
        for name, tensor in data_dict.items():
            out_data_dict[name] = data_dict[name].to(device)  # 将张量移动到指定设备
        yield out_data_dict  # 生成批次数据

我们定义了一个姓氏数据集类和一个批次生成器函数,用于处理和加载姓氏数据,以便在机器学习模型中使用。

class Vocabulary(object):
    """处理文本并提取用于映射的词汇的类"""
 
    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        参数:
            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)  # 添加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):
        """根据标记更新映射字典。
        
        参数:
            token (str): 要添加到词汇表中的项
        返回:
            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):
        """将标记列表添加到词汇表中
        
        参数:
            tokens (list): 字符串标记的列表
        返回:
            indices (list): 对应于这些标记的索引列表
        """
        return [self.add_token(token) for token in tokens]
 
    def lookup_token(self, token):
        """检索与标记关联的索引,如果标记不存在则返回UNK索引。
        
        参数:
            token (str): 要查找的标记
        返回:
            index (int): 对应于该标记的索引
        注意:
            `unk_index`需要 >=0(已添加到词汇表中)以实现UNK功能 
        """
        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):
        """返回与索引关联的标记
        
        参数: 
            index (int): 要查找的索引
        返回:
            token (str): 对应于该索引的标记
        引发:
            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)

这段代码定义了一个词汇表的类,用于处理文本并提取词汇表以进行映射。我们使用的是one-hot词汇表,不计算字符出现的频率,只对频繁出现的条目进行限制。

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  # 将姓氏中的每个字符在独热编码中置1
 
        return one_hot
 
    @classmethod
    def from_dataframe(cls, surname_df):
        """从数据集数据框实例化向量化器
        
        参数:
            surname_df (pandas.DataFrame): 姓氏数据集
        返回:
            SurnameVectorizer 的一个实例
        """
        surname_vocab = Vocabulary(unk_token="@")  # 创建带有UNK标记的姓氏词汇表
        nationality_vocab = Vocabulary(add_unk=False)  # 创建不带UNK标记的国籍词汇表
 
        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()}

这段代码定义了一个名为 SurnameVectorizer 的类,用于协调和使用多个词汇表。该类主要用于将姓氏转换为向量表示。

2.2关于CNN的数据预处理

尽管我们使用了来自“示例:带有多层感知器的姓氏分类”中的相同数据集,但在实现上有一个不同之处:数据集由onehot向量矩阵组成,而不是一个收缩的onehot向量。为此,我们实现了一个数据集类,它跟踪最长的姓氏,并将其作为矩阵中包含的行数提供给矢量化器。列的数量是onehot向量的大小(词汇表的大小)。

Vectorizer的vectorize()方法也要更改,以适应CNN模型的需要。现在,该函数将字符串中的每个字符映射到一个整数,然后使用这些整数构造一个由one-hot向量组成的矩阵,矩阵中的每一列都是不同的one-hot向量。这样设计的主要原因是将使用的Conv1d层要求数据张量在第0维上具有批处理,在第1维上具有通道,在第2维上具有特性。除了更改为使用one-hot矩阵之外,此代码还修改了矢量化器,以便计算姓氏的最大长度并将其保存。

class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        初始化姓氏数据集
        
        Args:
            surname_df (pandas.DataFrame): 数据集
            vectorizer (SurnameVectorizer): 从数据集实例化的向量化器
        """
        self.surname_df = surname_df
        self._vectorizer = vectorizer
        self.train_df = self.surname_df[self.surname_df.split=='train']
        self.train_size = len(self.train_df)
 
        self.val_df = self.surname_df[self.surname_df.split=='val']
        self.validation_size = len(self.val_df)
 
        self.test_df = self.surname_df[self.surname_df.split=='test']
        self.test_size = len(self.test_df)
 
        self._lookup_dict = {'train': (self.train_df, self.train_size),
                             'val': (self.val_df, self.validation_size),
                             'test': (self.test_df, self.test_size)}
 
        self.set_split('train')
        
        # 类别权重
        class_counts = surname_df.nationality.value_counts().to_dict()
        def sort_key(item):
            return self._vectorizer.nationality_vocab.lookup_token(item[0])
        sorted_counts = sorted(class_counts.items(), key=sort_key)
        frequencies = [count for _, count in sorted_counts]
        self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)
 
 
    @classmethod
    def load_dataset_and_make_vectorizer(cls, surname_csv):
        """加载数据集并从头创建新的向量化器
        
        Args:
            surname_csv (str): 数据集的位置
        Returns:
            SurnameDataset的一个实例
        """
        surname_df = pd.read_csv(surname_csv)
        train_surname_df = surname_df[surname_df.split=='train']
        return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
 
    @classmethod
    def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
        """加载数据集和相应的向量化器。 
        """

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): 与令牌对应的整数索引
        """
        if token not in self._token_to_idx:
            index = len(self._token_to_idx)
            self._token_to_idx[token] = index
            self._idx_to_token[index] = token
        else:
            index = self._token_to_idx[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): 与令牌对应的索引
        """
        return self._token_to_idx.get(token, self.unk_index)
 
    def lookup_index(self, index):
        """返回与索引关联的令牌
        
        Args:
            index (int): 要查找的索引
        Returns:
            token (str): 与索引对应的令牌
        Raises:
            KeyError: 如果索引不在词汇表中
        """
        if index not in self._idx_to_token:
            raise KeyError("索引 (%d) 不在词汇表中" % index)
        return self._idx_to_token[index]
 
    def __str__(self):
        return "<Vocabulary(size=%d)>" % len(self)
 
    def __len__(self):
        return len(self._token_to_idx)


class SurnameVectorizer(object):
    """姓氏向量化器,协调词汇表并将其应用到数据上"""
 
    def __init__(self, surname_vocab, nationality_vocab, max_surname_length):
        """
        Args:
            surname_vocab (Vocabulary): 将字符映射到整数的词汇表
            nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
            max_surname_length (int): 最长姓氏的长度
        """
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab
        self._max_surname_length = max_surname_length
 
    def vectorize(self, surname):
        """
        Args:
            surname (str): 姓氏
        Returns:
            one_hot_matrix (np.ndarray): 一个独热向量矩阵
        """
 
        # 初始化全零矩阵
        one_hot_matrix_size = (len(self.surname_vocab), self._max_surname_length)
        one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)
                               
        # 将姓氏中的每个字符转换为独热向量
        for position_index, character in enumerate(surname):
            character_index = self.surname_vocab.lookup_token(character)
            one_hot_matrix[character_index][position_index] = 1
        
        return one_hot_matrix
 
    @classmethod
    def from_dataframe(cls, surname_df):
        """从数据框实例化向量化器
        
        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)

三、前馈神经网络

从数据流动方向来看,从第二层神经元开始,每一层神经元都可以接收前一层的输出,并输出给下一层,即整体上数据从左到右逐层传递,信号从输入层到输出层单向传播。所以,从信号(数据)传递方向上看,可以称作是前馈神经网络(Feedforward neural network,FNN)

本次实验中我们使用的就是前馈神经网络中的多层感知机(MLP)

3.1多层感知机(MLP)

对于一个分类问题,我们最常用的方法就是感知机,一个二分类的线性可分问题,单层感知机是很容易想到的。但对于一个与或(XOR)问题,我们就需要多层感知机来解决了。

左边是单层感知机,右边是多层感知机。

多层感知机(Multilayer Perceptron,简称MLP),是一种基于前馈神经网络(Feedforward Neural Network)的深度学习模型,由多个神经元层组成,其中每个神经元层与前一层全连接。多层感知机可以用于解决分类、回归和聚类等各种机器学习问题。
多层感知机的每个神经元层由许多神经元组成,其中输入层接收输入特征,输出层给出最终的预测结果,中间的隐藏层用于提取特征和进行非线性变换。每个神经元接收前一层的输出,进行加权和和激活函数运算,得到当前层的输出。通过不断迭代训练,多层感知机可以自动学习到输入特征之间的复杂关系,并对新的数据进行预测。

一个单隐藏层的多层感知机

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

class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        参数:
            input_dim (int): 输入向量的大小
            hidden_dim (int): 第一个线性层的输出大小
            output_dim (int): 第二个线性层的输出大小
        """
        super(MultilayerPerceptron, 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):
        """MLP的前向传播

        参数:
            x_in (torch.Tensor): 输入数据张量。
                x_in的形状应为 (batch, input_dim)
            apply_softmax (bool): 是否应用softmax激活函数的标志
                如果使用交叉熵损失,应为False
        返回:
            结果张量。张量形状应为 (batch, output_dim)
        """
        intermediate = F.relu(self.fc1(x_in))  # 应用ReLU激活函数到第一个全连接层的输出
        output = self.fc2(intermediate)  # 第二个全连接层

        if apply_softmax:
            output = F.softmax(output, dim=1)  # 如果apply_softmax为True,应用softmax激活函数
        return output

我们用PyTorch的两个线性模块实例化了这个想法。线性对象被命名为fc1和fc2,它们遵循一个通用约定,即将线性模块称为“完全连接层”,简称为“fc层”。除了这两个线性层外,还有一个修正的线性单元(ReLU)非线性,它在被输入到第二个线性层之前应用于第一个线性层的输出。由于层的顺序性,必须确保层中的输出数量等于下一层的输入数量。使用两个线性层之间的非线性是必要的,因为没有它,两个线性层在数学上等价于一个线性层4,因此不能建模复杂的模式。

batch_size = 2 # number of samples input at once
input_dim = 3#设置输入纬度为3
hidden_dim = 100#设置隐藏维度为100
output_dim = 4#设置输出纬度为4

# Initialize model
mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
print(mlp)

设置多层感知机的参数,使用print查看模型结构

可以看到两个线性层,一个输入3维度,输出100维度,另一个输入100维度,输出3维度。

import torch
def describe(x):
    print("Type: {}".format(x.type()))#打印类型
    print("Shape/size: {}".format(x.shape))#打印大小
    print("Values: \n{}".format(x))#打印数值

x_input = torch.rand(batch_size, input_dim)
describe(x_input)

y_output = mlp(x_input, apply_softmax=False)
describe(y_output)

使用一个简单的代码查看模型是否完整,是否工作,此时模型还未训练,所以输出结果是随机的,只是作为一个可用的检测。

如果想将预测向量转换为概率,则需要额外的步骤。具体来说,需要softmax函数,它用于将一个值向量转换为概率。softmax有许多根。在物理学中,它被称为玻尔兹曼或吉布斯分布;在统计学中,它是多项式逻辑回归;在自然语言处理(NLP)社区,它是最大熵(MaxEnt)分类器。不管叫什么名字,这个函数背后的直觉是,大的正值会导致更高的概率,小的负值会导致更小的概率。这次将apply_softmax标志设置为True。

y_output = mlp(x_input, apply_softmax=True)
describe(y_output)

3.2 MLP进行姓氏分类

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  # 返回预测结果张量

此段代码构建了一个用于姓氏分类的两层多层感知器(MLP)。

def create_surname_vocab(data):
    # 从数据中提取姓氏,并创建姓氏词汇表
    surname_vocab = set()
    for example in data:
        surname = example['surname']
        surname_vocab.add(surname)
    return list(surname_vocab)
 
# 现在 train_df, val_df, test_df 分别包含了训练集、验证集和测试集的数据
 
csv_file_path = 'surnames.csv'
 
# 使用 pandas 的 read_csv 函数加载数据
surname_df = pd.read_csv(csv_file_path)
# 使用 train_test_split 进行数据分割
train_df, test_df = train_test_split(surname_df, test_size=0.2, random_state=42)  # 80% 训练集,20% 测试集
val_df, test_df = train_test_split(test_df, test_size=0.5, random_state=42)  # 从测试集中分割出验证集和测试集
# 创建 SurnameVectorizer 实例
vectorizer = SurnameVectorizer.from_dataframe(train_df)  # 通常使用训练集来创建向量化器
 
# 创建 SurnameDataset 实例
train_dataset = SurnameDataset(train_df, vectorizer)
val_dataset = SurnameDataset(val_df, vectorizer)
test_dataset = SurnameDataset(test_df, vectorizer)
 
# 创建 DataLoader 对象
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
 
# 现在 surname_df 是一个包含 CSV 文件数据的 pandas DataFrame
 
vectorizer = SurnameVectorizer.from_dataframe(surname_df)
 
args = {
    # 数据和路径信息
    'surname_csv': 'surnames.csv',  # 姓氏数据的CSV文件路径
    'vectorizer_file': 'vectorizer.json',  # 向量化器的保存路径
    'model_state_file': 'model.pth',  # 模型状态的保存路径
    'save_dir': 'model_storage/ch4/surname_mlp',  # 模型保存目录
    # 模型超参数
    'hidden_dim': 300,  # 隐藏层的维度
    # 训练超参数
    'seed': 1337,  # 随机种子
    'num_epochs': 100,  # 训练轮数
    'early_stopping_criteria': 5,  # 提前停止的条件
    'learning_rate': 0.001,  # 学习率
    'batch_size': 64,  # 批量大小
    # 省略了运行时选项以节省空间
}
 
 
# 使用 vectorizer 对象创建 SurnameClassifier 实例
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab),
                               hidden_dim=args['hidden_dim'],
                               output_dim=len(vectorizer.nationality_vocab))

读取数据集,设定好训练参数。

# 定义损失函数,这里以交叉熵损失为例
loss_func = nn.CrossEntropyLoss()
# 接下来,使用 num_epochs 执行训练循环
for epoch in range(num_epochs):
    for batch in train_loader:
        optimizer.zero_grad()
        x_surname = batch['x_surname']
        y_nationality = batch['y_nationality']
        
        # 前向传播
        y_pred = classifier(x_surname)
        
        # 计算损失
        loss = loss_func(y_pred, y_nationality)
        
        # 反向传播和优化
        loss.backward()
        optimizer.step()

选择交叉熵损失函数,建立循环,通过反向传播来训练模型

def predict_nationality(name, classifier, vectorizer):
    # 将名称转换为向量表示
    vectorized_name = vectorizer.vectorize(name)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
 
    # 使用分类器进行预测
    result = classifier(vectorized_name, apply_softmax=True)
 
    # 获取预测结果的概率值和索引
    probability_values, indices = result.max(dim=1)
    index = indices.item()
 
    # 从向量化器中查找预测的国籍
    predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
    probability_value = probability_values.item()
 
    return {'nationality': predicted_nationality,
            'probability': probability_value}
 
new_surname = input("Enter a surname to classify: ")
classifier = classifier.to("cpu")
prediction = predict_nationality(new_surname, classifier, vectorizer)
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

设定一个预测函数,让我们可以实验模型效果。

四、卷积神经网络

卷积神经网络(Convolutional Neural Networks, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络(Feedforward Neural Networks),是深度学习(deep learning)的代表算法之一 [1-2]。卷积神经网络具有表征学习(representation learning)能力,能够按其阶层结构对输入信息进行平移不变分类(shift-invariant classification)

卷积神经网络的基本结构大致包括:卷积层、激活函数、池化层、全连接层、输出层等。

4.1卷积神经网络结构介绍

4.1.1卷积层

卷积层(Convolutional layer),这一层就是卷积神经网络最重要的一个层次,也是“卷积神经网络”的名字来源。卷积神经网路中每层卷积层由若干卷积单元组成,每个卷积单元的参数都是通过反向传播算法优化得到的。 

4.1.2激活函数

激活函数,最常用的激活函数目前有Relu、tanh、sigmoid。

 Sigmoid

性质:

  • 输出值范围为(0, 1)。
  • 常用于二分类问题。
  • 可能导致梯度消失问题,尤其是在深层网络中。

图像:

Tanh(Hyperbolic Tangent)

性质:

  • 输出值范围为(-1, 1)。
  • 对称于原点(0,0),有利于加速收敛。
  • 在输入非常大或非常小时,输出接近1或-1,可能导致梯度消失。

图像

ReLU(Rectified Linear Unit)

性质:

  • 当输入大于0时,输出等于输入;当输入小于或等于0时,输出为0。
  • 计算简单,收敛速度快。
  • 但在负轴上的输入会导致神经元“死亡”,即这些神经元在整个训练过程中都不会被激活。

图像:

4.1.3池化层

池化层(Pooling layer),通常在卷积层之后会得到维度很大的特征,将特征切成几个区域,取其最大值或平均值,得到新的、维度较小的特征。最常用的是最大池化和平均池化。

4.1.4全连接层

全连接层( Fully-Connected layer), 把所有局部特征结合变成全局特征,用来计算最后每一类的得分。

        全连接层往往在分类问题中用作网络的最后层,作用主要为将数据矩阵进行全连接,然后按照分类数量输出数据,在回归问题中,全连接层则可以省略,但是我们需要增加卷积层来对数据进行逆卷积操作。

4.1.5输出层

输出层主要准备做好最后目标结果的输出。

4.2使用CNN进行姓氏分类

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(),  # ELU激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3, stride=2),  # 第二个卷积层,步长为2
            nn.ELU(),
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels, 
                      kernel_size=3, stride=2),  # 第三个卷积层,步长为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  # 返回预测结果张量

构建一个CNN的模型。

classifier = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)
 
# 定义损失函数和优化器
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
                                                 mode='min', factor=0.5,
                                                 patience=1)
 
# 初始化训练状态
train_state = make_train_state(args)
 
# 创建进度条
epoch_bar = tqdm_notebook(desc='training routine', 
                          total=args.num_epochs,
                          position=0)
 
# 设置训练和验证数据集
dataset.set_split('train')
train_bar = tqdm_notebook(desc='split=train',
                          total=dataset.get_num_batches(args.batch_size), 
                          position=1, 
                          leave=True)
dataset.set_split('val')
val_bar = tqdm_notebook(desc='split=val',
                        total=dataset.get_num_batches(args.batch_size), 
                        position=1, 
                        leave=True)
 
try:
    for epoch_index in range(args.num_epochs):
        train_state['epoch_index'] = epoch_index
 
        # 训练阶段
        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)
 
        # 验证阶段
        dataset.set_split('val')
        batch_generator = generate_batches(dataset, 
                                           batch_size=args.batch_size, 
                                           device=args.device)
        running_loss = 0.
        running_acc = 0.
        classifier.eval()
 
        for batch_index, batch_dict in enumerate(batch_generator):
            # 计算输出
            y_pred =  classifier(batch_dict['x_surname'])
            # 计算损失
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.to("cpu").item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)
            # 计算准确率
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)
            val_bar.set_postfix(loss=running_loss, acc=running_acc, 
                            epoch=epoch_index)
            val_bar.update()
 
        train_state['val_loss'].append(running_loss)
        train_state['val_acc'].append(running_acc)
 
        # 更新训练状态
        train_state = update_train_state(args=args, model=classifier,
                                         train_state=train_state)
 
        # 更新学习率
        scheduler.step(train_state['val_loss'][-1])
 
        # 如果满足提前停止条件, 则退出循环
        if train_state['stop_early']:
            break
 
        # 重置进度条
        train_bar.n = 0
        val_bar.n = 0
        epoch_bar.update()
except KeyboardInterrupt:
    print("Exiting loop")

模型进行训练,设定好所需参数

def predict_nationality(surname, classifier, vectorizer):
    """预测一个新姓氏的国籍
    
    Args:
        surname (str): 要分类的姓氏
        classifier (SurnameClassifer): 分类器的实例
        vectorizer (SurnameVectorizer): 相应的向量化器
    
    Returns:
        dict: 包含最可能的国籍及其概率的字典
    """
    # 向量化姓氏
    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}
 
 
csv_file_path = 'surnames.csv'
surname_df = pd.read_csv(csv_file_path)
 
 
vectorizer = SurnameVectorizer.from_dataframe(surname_df)
initial_num_channels = 82  # 姓氏特征向量的大小
num_classes = 18  # 国籍的类别数
num_channels = 64  # 网络中使用的通道数
# 初始化分类器
classifier = SurnameClassifier(initial_num_channels, num_classes, num_channels)
# 将分类器移到 CPU 上进行推理
classifier = classifier.cpu()
 
# 获取用户输入并进行预测
new_surname = input("Enter a surname to classify: ")
prediction = predict_nationality(new_surname, classifier, vectorizer)
 
# 打印预测结果
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

predict_nationality()函数的部分已被修改,不再使用view方法重塑新创建的数据张量以添加批处理维度,而是使用PyTorch的unsqueeze()函数在应该添加大小为1的维度的位置上添加批处理维度。

五、总结

本次实验所使用的两个模型都比较简单,也只是简单完成了一遍流程。在数据预处理步骤,由于两种模型所需的数据不同,所以构建的数据提取器也有所不同,请读者多加注意。如果你想要得到更好的结果可以尝试使用别的复杂些的模型,和更大的数据集。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值