自然语言处理前馈网络

1 自然语言处理前馈网络(Feed Forward Neural Network,FNN)

一、定义与原理

前馈神经网络是一种最基本的神经网络,其信息流只能从输入层到输出层,不能在网络中形成闭环。具体来说,输入数据沿着网络的输入层传递到隐藏层,最终到达输出层。在每个神经元中,输入的加权和经过激活函数后得到输出,作为下一层的输入。这种网络结构简单,易于实现,且可以通过简单非线性函数的多次复合,实现输入空间到输出空间的复杂映射。

二、特点

  1. 计算速度快:前馈神经网络由于其单向传播的特性,计算速度相对较快,适用于实时应用。
  2. 容易实现和训练:前馈神经网络的结构相对简单,易于实现和训练。
  3. 可以处理高维数据:前馈神经网络能够处理高维数据,这在自然语言处理中尤为重要,因为文本数据通常是高维的。

三、应用

前馈神经网络在自然语言处理中有着广泛的应用,包括但不限于以下几个方面:

  1. 文本分类:前馈神经网络可以用于文本分类任务,如情感分析、主题分类等。通过将文本数据转换为向量表示,前馈神经网络可以学习文本数据的特征,并将其映射到不同的类别中。
  2. 实体识别:在命名实体识别(Named Entity Recognition,NER)任务中,前馈神经网络可以学习文本中的实体边界和类型,从而识别出文本中的实体。
  3. 机器翻译:虽然前馈神经网络在机器翻译任务中不如循环神经网络(RNN)和变压器(Transformer)等模型表现突出,但在某些特定的翻译任务中,前馈神经网络仍然可以发挥一定的作用。

2 The Multilayer Perceptron(多层感知器)

多层感知器(MLP)被认为是最基本的神经网络构建模块之一。最简单的MLP是对第3章感知器的扩展。感知器将数据向量作为输入,计算出一个输出值。在MLP中,许多感知器被分组,以便单个层的输出是一个新的向量,而不是单个输出值。在PyTorch中,正如您稍后将看到的,这只需设置线性层中的输出特性的数量即可完成。MLP的另一个方面是,它将多个层与每个层之间的非线性结合在一起。

最简单的MLP,如图所示,由三个表示阶段和两个线性层组成。第一阶段是输入向量。这是给定给模型的向量。在“示例:对餐馆评论的情绪进行分类”中,输入向量是Yelp评论的一个收缩的one-hot表示。给定输入向量,第一个线性层计算一个隐藏向量——表示的第二阶段。隐藏向量之所以这样被调用,是因为它是位于输入和输出之间的层的输出。我们所说的“层的输出”是什么意思?理解这个的一种方法是隐藏向量中的值是组成该层的不同感知器的输出。使用这个隐藏的向量,第二个线性层计算一个输出向量。在像Yelp评论分类这样的二进制任务中,输出向量仍然可以是1。在多类设置中,将在本实验后面的“示例:带有多层感知器的姓氏分类”一节中看到,输出向量是类数量的大小。虽然在这个例子中,我们只展示了一个隐藏的向量,但是有可能有多个中间阶段,每个阶段产生自己的隐藏向量。最终的隐藏向量总是通过线性层和非线性的组合映射到输出向量。

优点

  • 具有较强的表达能力,可以处理非线性问题和高维数据。
  • 可以通过反向传播算法进行训练,自动学习特征和模式。
  • 可以处理多分类问题和回归问题,具有较好的泛化能力。
  • 可以通过添加正则化项、dropout等技术来防止过拟合。

缺点

  • 训练时间较长,需要大量的计算资源和时间。
  • 对初始权重和偏置的选择比较敏感,可能会导致模型陷入局部最优解。
  • 对数据的标准化和预处理要求较高,需要进行归一化、标准化等处理。
  • 难以解释和理解,不如决策树等模型具有可解释性。

应用场景

多层感知器具有较强的表达能力和泛化能力,可以处理非线性问题和高维数据,因此在许多领域都有广泛的应用,包括但不限于:

  • 计算机视觉:图像分类、目标检测、图像分割等。
  • 自然语言处理:文本分类、情感分析、机器翻译等。
  • 医疗领域:疾病诊断和药物研发。
  • 金融领域:股票价格预测和风险评估。
  • 能源领域:能源消耗预测和节能优化。

2.1感知器和MLP学习的XOR问题的解决方案显示

 在这个例子中,我们在一个二元分类任务中训练感知器和MLP:星和圆。每个数据点是一个二维坐标。在不深入研究实现细节的情况下,最终的模型预测如图4-3所示。在这个图中,错误分类的数据点用黑色填充,而正确分类的数据点没有填充。在左边的面板中,从填充的形状可以看出,感知器在学习一个可以将星星和圆分开的决策边界方面有困难。然而,MLP(右面板)学习了一个更精确地对恒星和圆进行分类的决策边界。

每个数据点的真正类是该点的形状:星形或圆形。错误的分类用块填充,正确的分类没有填充。这些线是每个模型的决策边界。在边的面板中,感知器学习—个不能正确地将圆与星分开的决策边界。事实上,没有一条线可以。在右动的面板中,MLP学会了从圆中分离星。

2.2 PyTorch的一个实现

在PyTorch中,我们使用两个线性模块(通常被称为“完全连接层”或简称“fc层”)来构建一个简单的神经网络结构。这些线性层分别被命名为fc1fc2。在fc1fc2之间,我们引入了一个修正的线性单元(ReLU)作为非线性激活函数,它有助于模型学习复杂的非线性关系。为了保持网络结构的连贯性,必须确保fc1的输出特征数量与fc2的输入特征数量相匹配。这种层与层之间的连接确保了信息的有效传递。代码如下

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

seed = 1337

torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
import torch.nn as nn
import torch.nn.functional as F

class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        初始化多层感知机模型,包括两个全连接层。
        
        Args:
            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):
        """
        多层感知机的前向传播过程。
        
        Args:
            x_in (torch.Tensor): 输入数据张量。
                x_in.shape 应该是 (batch, input_dim)
            apply_softmax (bool): softmax激活函数的标志
                如果与交叉熵损失一起使用,应为false
        Returns:
            结果张量。tensor.shape 应该是 (batch, output_dim)
        """
        intermediate = F.relu(self.fc1(x_in))  # 第一层全连接层经过ReLU激活函数处理
        output = self.fc2(intermediate)  # 第二层全连接层

        if apply_softmax:
            output = F.softmax(output, dim=1)  # 如果需要应用softmax激活函数,则进行softmax处理
        return output
batch_size = 2 # number of samples input at once
input_dim = 3
hidden_dim = 100
output_dim = 4

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

使用大小为3的输入维度、大小为4的输出维度和大小为100的隐藏维度。
结果如下:

在一个多层感知器(MLP)模型的示例中,模型的输出是一个张量,其形状通常取决于模型的配置和输入数据。具体来说,输出张量的行数通常对应于批量处理中数据点的数量,而列数则代表了每个数据点通过模型转换后得到的最终特征表示或预测向量的维度。

在分类任务中,这些特征向量往往被解释为预测向量,它们表示了模型对于不同类别的预测概率分布。预测向量的后续处理取决于模型当前所处的阶段:训练或推理。在训练阶段,预测向量会与真实的目标类标签一起输入到损失函数中,以计算预测与实际之间的误差,从而指导模型的权重更新。而在推理或预测阶段,这些预测向量可能经过额外的处理(如选择最大概率的类别)以得到最终的预测结果。

3 带有多层感知器的姓氏分类

带有多层感知器(MLP)的姓氏分类是一个利用多层前馈神经网络来解决分类问题的应用实例。姓氏分类是一个典型的分类问题,目标是将给定的姓氏数据正确归类到相应的类别中。多层感知器(MLP)作为神经网络的一种,具有强大的非线性映射能力和学习能力,适合用于解决此类问题。

3.1数据准备

姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,这些姓氏是作者从互联网上不同的姓名来源收集的。该数据集将在本课程实验的几个示例中重用,并具有一些使其有趣的属性。第一个性质是它是相当不平衡的。排名前三的课程占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。剩下的15个民族的频率也在下降——这也是语言特有的特性。

数据收集:收集包含不同姓氏的数据集,每个姓氏都与一些特征相关联。在姓氏分类问题中,姓氏本身可能就是唯一的特征,但为了演示,可以假设有其他的辅助特征。

数据预处理:对收集到的数据进行预处理,包括缺失值填充、数据归一化等。归一化是将数据缩放到同一尺度,有助于加快模型的训练速度和提高性能。

数据划分:将数据集划分为训练集、验证集和测试集。通常,训练集用于训练模型,验证集用于调整超参数和评估模型性能,

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

import numpy as np
import pandas as pd

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm_notebook
class 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的生成器函数。它将确保每个张量在正确的设备位置上。
    """
    dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
                            shuffle=shuffle, drop_last=drop_last)

    for data_dict in dataloader:
        out_data_dict = {}
        for name, tensor in data_dict.items():
            out_data_dict[name] = data_dict[name].to(device)
        yield out_data_dict
class Vocabulary(object):
    """用于处理文本并提取词汇以进行映射的类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        初始化Vocabulary实例。

        参数:
            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
        # 如果要求,将UNK标记添加到词汇表中
        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 (str): 要添加到Vocabulary中的项
        返回:
            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):
        """将标记列表添加到Vocabulary中。
        
        参数:
            tokens (list): 字符串标记列表
        返回:
            indices (list): 与标记对应的索引列表
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """检索与标记相关联的索引,如果标记不存在,则使用UNK索引。
        
        参数:
            token (str): 要查找的标记
        返回:
            index (int): 与标记相关的索引
        注意:
            UNK功能需要unk_index >= 0(已添加到Vocabulary中)
        """
        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: 如果索引不在Vocabulary中
        """
        if index not in self._idx_to_token:
            raise KeyError("索引(%d)不在Vocabulary中" % index)
        return self._idx_to_token[index]

    def __str__(self):
        """返回Vocabulary的字符串表示形式。"""
        return "<Vocabulary(size=%d)>" % len(self)

    def __len__(self):
        """返回Vocabulary中唯一标记的数量。"""
        return len(self._token_to_idx)
class SurnameVectorizer(object):
    """协调Vocabularies并将它们应用于实际用途的向量化器"""

    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):
        """
        将姓氏向量化为一种折叠的one-hot编码。

        参数:
            surname (str): 姓氏

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

        return one_hot

    @classmethod
    def from_dataframe(cls, surname_df):
        """从数据集DataFrame实例化向量化器。

        参数:
            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()}

测试集用于最终评估模型的泛化能力。

3.2构建多层感知器模型

  • 输入层:输入层的节点数等于特征的数量,每个节点代表一个特征。
  • 隐藏层:一个或多个隐藏层,每个隐藏层由多个神经元组成。隐藏层的目的是提取更高层次的特征。
  • 激活函数:在隐藏层中,每个神经元通常会使用非线性激活函数,如ReLU(Rectified Linear Unit)或Sigmoid函数,以引入非线性,增强模型的表达能力。
  • 输出层:输出层的节点数等于分类的类别数。通常使用Softmax激活函数,将输出转换为概率分布,表示每个类别的预测概率。
def make_train_state(args):
    """
    初始化训练状态字典
    
    参数:
        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):
    """
    处理训练状态更新

    组件:
     - 早停: 防止过拟合
     - 模型检查点: 如果模型更好则保存模型

    参数:
        args: 主参数
        model: 要训练的模型
        train_state: 表示训练状态值的字典
    返回:
        更新后的训练状态字典
    """

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

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

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

            # 重置早停步骤
            train_state['early_stopping_step'] = 0

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

    return train_state

def compute_accuracy(y_pred, y_target):
    """
    计算准确率
    
    参数:
        y_pred (torch.Tensor): 预测的张量
        y_target (torch.Tensor): 目标张量
    返回:
        准确率的百分比
    """
    _, y_pred_indices = y_pred.max(dim=1)  # 获取预测的最大值的索引
    n_correct = torch.eq(y_pred_indices, y_target).sum().item()  # 计算正确预测的数量
    return n_correct / len(y_pred_indices) * 100  # 返回准确率百分比

这段代码包含了三个函数,它们在机器学习训练过程中扮演着不同的角色。下面是对每个函数的分析:

  1. make_train_state(args) 函数:

    • 作用:初始化训练状态字典。
    • 参数:args,主参数,通常是一个包含训练配置的命名空间或对象。
    • 返回值:一个字典,包含训练过程中的各种状态信息,如是否早停、早停计数、最佳验证损失、学习率、当前训练轮次、训练和验证的损失和准确率列表,以及测试损失和准确率和模型文件名。
  2. update_train_state(args, model, train_state) 函数:

    • 作用:更新训练状态,包括早停机制和模型检查点保存。
    • 参数:
      • args:主参数,包含训练配置。
      • model:当前训练的模型。
      • train_state:表示训练状态的字典。
    • 返回值:更新后的训练状态字典。
    • 功能细节:
      • 如果是第一个训练轮次(epoch_index 为 0),则保存模型的当前状态字典到文件。
      • 如果不是第一个训练轮次,比较当前轮次的验证损失与上一轮次的损失,如果当前损失大于之前记录的最佳验证损失,则增加早停计数;如果当前损失小于最佳验证损失,则保存当前模型状态为最佳模型,并重置早停计数。
      • 如果早停计数达到 args.early_stopping_criteria 指定的阈值,则设置 stop_early 为 True,表示训练应该提前终止以避免过拟合。
  3. compute_accuracy(y_pred, y_target) 函数:

    • 作用:计算给定预测和目标张量的准确率。
    • 参数:
      • y_pred:模型预测的张量,通常是一个概率分布。
      • y_target:实际的目标张量。
    • 返回值:准确率的百分比。
    • 功能细节:
      • 使用 max 函数找到 y_pred 中概率最大的索引,这代表了模型的预测类别。
      • 计算这些预测索引与 y_target 中的实际索引相等的数量,即正确预测的数量。
      • 计算准确率,即正确预测的数量除以总预测数量,然后乘以 100 得到百分比。

3.3训练模型

  1. 学习率调度器初始化

    使用ReduceLROnPlateau调度器,当验证集上的性能(如损失)不再提升时,减少学习率。
  2. 初始化训练状态

    创建一个字典train_state来存储训练过程中的状态信息。
  3. 进度条设置

    使用tqdm库设置训练和验证过程的进度条,以便于监控训练进度。
  4. 训练循环

    循环迭代args.num_epochs次,每次迭代代表一个训练周期。
  5. 训练数据集迭代

    设置数据集为训练模式,创建批处理生成器,初始化损失和准确率,设置模型为训练模式。对于每个批次,执行以下步骤:
    • 计算准确率并更新进度条。
    • 更新优化器,进行梯度下降。
    • 计算损失并反向传播。
    • 计算模型输出y_pred
    • 清零梯度。
  6. 验证数据集迭代

    • 设置数据集为验证模式,创建批处理生成器,初始化损失和准确率,设置模型为评估模式。
    • 类似于训练数据集迭代,但不对模型参数进行更新,只计算损失和准确率。
  7. 更新训练状态

    根据验证损失更新训练状态,可能包括早停(early stopping)等策略。
  8. 学习率调整

    使用scheduler.step()根据验证损失调整学习率。
  9. 早停检查

    如果满足早停条件,则退出训练循环。
  10. 进度条重置

    重置训练和验证进度条的计数器。
  11. 异常处理

    使用try-except结构来捕获KeyboardInterrupt,允许用户通过中断信号(如Ctrl+C)安全退出训练循环。

 

scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
                                                 mode='min', factor=0.5,
                                                 patience=1)
# 初始化训练状态
train_state = make_train_state(args)

epoch_bar = tqdm(desc='training routine',   # 迭代轮次
                 total=args.num_epochs,
                 position=0)

dataset.set_split('train')
train_bar = tqdm(desc='split=train',
                 total=dataset.get_num_batches(args.batch_size), 
                 position=1, 
                 leave=True)
dataset.set_split('val')
val_bar = tqdm(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

        # Iterate over training dataset

      
        #  设置: 批处理生成器, 将损失和准确率设为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):
            # the training routine is these 5 steps:

            # --------------------------------------
            # 步骤 1. 清零梯度
            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()

            # step 5. use optimizer to take gradient step
            optimizer.step()
            # -----------------------------------------
            # compute the accuracy
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)

            # update bar
            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)

        # Iterate over val dataset

        # setup: batch generator, set loss and acc to 0; set eval mode on
        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")

训练过程:

3.4 应用模型

预测新姓氏:使用训练好的模型来预测新姓氏的分类。

代码实现了一个基于深度学习模型的姓氏民族预测功能,允许用户输入一个姓氏,并得到模型预测的该姓氏最可能属于的k个民族及其概率。代码中还包含了用户交互的部分,使得整个过程更加友好和直观。

 

def predict_topk_nationality(name, classifier, vectorizer, k=5):
    # 将名字向量化
    vectorized_name = vectorizer.vectorize(name)
    # 将向量转换为张量并调整形状
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    # 使用分类器对名字进行预测,并应用softmax函数
    prediction_vector = classifier(vectorized_name, apply_softmax=True)
    # 获取前k个最大概率值和对应的索引
    probability_values, indices = torch.topk(prediction_vector, k=k)

    # returned size is 1,k
    # 将概率值和索引转换为numpy数组
    probability_values = probability_values.detach().numpy()[0]
    indices = indices.detach().numpy()[0]

    # 初始化结果列表
    results = []
    # 遍历概率值和索引,将民族和概率值添加到结果列表中
    for prob_value, index in zip(probability_values, indices):
        nationality = vectorizer.nationality_vocab.lookup_index(index)
        results.append({'nationality': nationality,
                        'probability': prob_value})

    return results
# 输入要分类的姓氏
new_surname = input("Enter a surname to classify: ")

# 将分类器移到CPU上
classifier = classifier.to("cpu")

# 询问用户要显示的前k个预测结果
k = int(input("How many of the top predictions to see? "))
if k > len(vectorizer.nationality_vocab):
    print("Sorry! That's more than the # of nationalities we have.. defaulting you to max size :)")
    k = len(vectorizer.nationality_vocab)
    
# 调用函数进行预测
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)

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

结果预测:

 

 该模型显示wu为中国姓氏的可能性最大

4 使用卷积神经网络对姓氏进行分类(Convolutional Neural Network)

 使用CNN对姓氏进行分类的优势在于其强大的特征提取能力,可以自动学习姓氏中的关键特征,而不需要手动设计特征。然而,对于文本数据,卷积神经网络可能不如循环神经网络(RNN)或Transformer等模型在处理序列数据方面灵活。不过,CNN在某些特定的文本分类任务中仍然表现出色。

代码展示(仅展示训练部分)

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):
    """Handle the training state updates.

    Components:
     - Early Stopping: Prevent overfitting.
     - Model Checkpoint: Model is saved if the model is better

    :param args: main arguments
    :param model: model to train
    :param train_state: a dictionary representing the training state values
    :returns:
        a new train_state
    """

    # Save one model at least
    if train_state['epoch_index'] == 0:
        torch.save(model.state_dict(), train_state['model_filename'])
        train_state['stop_early'] = False

    # Save model if performance improved
    elif train_state['epoch_index'] >= 1:
        loss_tm1, loss_t = train_state['val_loss'][-2:]

        # If loss worsened
        if loss_t >= train_state['early_stopping_best_val']:
            # Update step
            train_state['early_stopping_step'] += 1
        # Loss decreased
        else:
            # Save the best model
            if loss_t < train_state['early_stopping_best_val']:
                torch.save(model.state_dict(), train_state['model_filename'])

            # Reset early stopping step
            train_state['early_stopping_step'] = 0

        # Stop early ?
        train_state['stop_early'] = \
            train_state['early_stopping_step'] >= args.early_stopping_criteria

    return train_state
def compute_accuracy(y_pred, y_target):
    y_pred_indices = y_pred.max(dim=1)[1]
    n_correct = torch.eq(y_pred_indices, y_target).sum().item()
    return n_correct / len(y_pred_indices) * 100
args = Namespace(
    # 数据和路径信息
    surname_csv="data/surnames/surnames_with_splits.csv",
    vectorizer_file="vectorizer.json",
    model_state_file="model.pth",
    save_dir="model_storage/ch4/cnn",
    # 模型超参数
    hidden_dim=100,
    num_channels=256,
    # 训练超参数
    seed=1337,
    learning_rate=0.001,
    batch_size=128,
    num_epochs=100,
    early_stopping_criteria=5,
    dropout_p=0.1,
    # 运行时选项
    cuda=False,
    reload_from_files=False,
    expand_filepaths_to_save_dir=True,
    catch_keyboard_interrupt=True
)

# 如果需要将文件路径扩展到保存目录,则进行扩展
if args.expand_filepaths_to_save_dir:
    args.vectorizer_file = os.path.join(args.save_dir,
                                        args.vectorizer_file)

    args.model_state_file = os.path.join(args.save_dir,
                                         args.model_state_file)
    
    print("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)

# 将分类器移动到指定设备(CPU或GPU)
classifer = classifier.to(args.device)
# 将类别权重移动到指定设备(CPU或GPU)
dataset.class_weights = dataset.class_weights.to(args.device)

# 定义损失函数,使用加权交叉熵损失
loss_func = nn.CrossEntropyLoss(weight=dataset.class_weights)
# 定义优化器,使用Adam算法
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

        # Iterate over training dataset

        # setup: batch generator, set loss and acc to 0, set train mode on

        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):
            # the training routine is these 5 steps:

            # --------------------------------------
            # step 1. zero the gradients
            optimizer.zero_grad()

            # step 2. compute the output
            y_pred = classifier(batch_dict['x_surname'])

            # step 3. compute the loss
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)

            # step 4. use loss to produce gradients
            loss.backward()

            # step 5. use optimizer to take gradient step
            optimizer.step()
            # -----------------------------------------
            # compute the accuracy
            acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
            running_acc += (acc_t - running_acc) / (batch_index + 1)

            # update bar
            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)

        # Iterate over val dataset

        # setup: batch generator, set loss and acc to 0; set eval mode on
        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):

            # compute the output
            y_pred =  classifier(batch_dict['x_surname'])

            # step 3. compute the loss
            loss = loss_func(y_pred, batch_dict['y_nationality'])
            loss_t = loss.item()
            running_loss += (loss_t - running_loss) / (batch_index + 1)

            # compute the accuracy
            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")

训练过程:

加载预训练的模型,并使用测试数据集评估模型的性能

结果如下

相同姓氏(wu)此模型训练结果:

再该模型中wu是韩国姓氏的可能性最大

5 总结

多层感知器(MLP)和卷积神经网络(CNN)都是深度学习模型,可以用于对姓氏进行分类任务,但它们在结构和处理数据的方式上存在一些差异。以下是对这两种模型在姓氏分类任务上的总结:

多层感知器(MLP):

  1. 结构简单:MLP由输入层、一个或多个隐藏层和输出层组成,每层由多个神经元构成。
  2. 全连接:在MLP中,每个神经元与前一层的所有神经元都有连接。
  3. 适用性:适用于处理表格数据,如经过特征工程处理的姓氏数据。
  4. 特征提取:需要手动设计特征或使用嵌入层自动学习特征。
  5. 训练速度:通常比CNN训练速度快,因为参数较少。
  6. 正则化:容易受到过拟合的影响,需要使用如Dropout、L1/L2正则化等技术来提高泛化能力。

卷积神经网络(CNN):

  1. 局部感知能力:CNN通过卷积层能够捕捉局部特征,适用于具有局部相关性的数据,如图像和文本。
  2. 参数共享:卷积核的权重在整个输入上共享,减少了模型的参数数量。
  3. 自动特征提取:CNN能够自动学习数据中的局部特征,减少了手动特征工程的需求。
  4. 多维数据处理:CNN天然适合处理具有空间结构的数据,如图像,但也可以处理一维序列数据,如文本。
  5. 池化层:通过池化层降低特征维度,提取主要特征。
  6. 训练速度:由于参数共享和现代硬件优化,CNN在图像处理任务上通常训练速度快。

姓氏分类任务中的考虑:

  • 数据特性:如果姓氏数据具有明显的局部特征或模式,CNN可能更有效。如果数据是表格形式的,MLP可能更简单。
  • 特征工程:MLP可能需要更多的特征工程,而CNN可以自动学习特征。
  • 泛化能力:CNN由于其结构特点,可能在某些情况下提供更好的泛化能力。
  • 训练资源:CNN可能需要更多的计算资源,特别是在大规模数据集上。
  • 模型复杂性:MLP结构简单,易于理解和实现;CNN结构可能更复杂,但提供了更强大的特征提取能力。

在选择MLP或CNN进行姓氏分类时,需要考虑任务的具体需求、数据的特性、可用的计算资源以及期望的模型性能。在实践中,可能需要尝试不同的模型和配置,以找到最适合特定任务的解决方案。

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值