NLP——CNN处理姓氏分类


前言

这是一篇实验博客——数据集、代码均未完整公示,谨展示基本训练流程,请客官谨慎食用。感谢Dong的introduction,可以让我轻松补充文字说明。

本次实验环境:

  • Python 3.6.7
  • 本次实验基于torch架构,适合具有一定基础的读者阅读。
  • 本次实验适合具有一定NLP基础的读者阅读。

一、CNN是什么?

卷积神经网络(Convolutional Neural Network,CNN)是一种在计算机视觉领域取得了巨大成功的深度学习模型。它们的设计灵感来自于生物学中的视觉系统,旨在模拟人类视觉处理的方式。在过去的几年中,CNN已经在图像识别、目标检测、图像生成和许多其他领域取得了显著的进展,成为了计算机视觉和深度学习研究的重要组成部分。本次博客我将介绍使用CNN来进行姓氏分类。

对于常用的图像分类任务,通常是堆叠多层卷积+池化的组合,现在更常用的操作是Conv+BatchNorm+ReLu+Pooling的组合,经过多层卷积后将feature map拉伸成一维向量喂入全连接层,经过全连接层得到一个预测输出,对于多分类任务,通常选取softmax+交叉熵损失函数。

注意本篇博客并未介绍softmax、交叉熵损失函数,如若有不了解可以自行查询资料。

二、关于CNN的基础知识(小白必看)

1.Filter(卷积核)

要了解CNN,我们首先要知道卷积核的概念。

在卷积神经网络中,卷积操作是指将一个可移动的小窗口与图像进行逐元素相乘然后相加的操作。这个小窗口其实是一组固定的权重,它可以被看作是一个特定的滤波器(filter)或卷积核。这个操作的名称“卷积”,源自于这种元素级相乘和求和的过程。这一操作是卷积神经网络名字的来源。

这张图中蓝色的框就是指一个数据窗口,红色框为卷积核(滤波器),最后得到的绿色方形就是卷积的结果(数据窗口中的数据与卷积核逐个元素相乘再求和),这是一次经典的Conv2D计算,我们本次实验使用Conv1D。

2.Channel(通道数)

通道(channel)是指沿输入中的每个点的特征维度。例如,在图像中,对应于RGB组件的图像中的每个像素有三个通道。在使用卷积时,文本数据也可以采用类似的概念。从概念上讲,如果文本文档中的“像素”是单词,那么通道的数量就是词汇表的大小。如果我们更细粒度地考虑字符的卷积,通道的数量就是字符集的大小(在本例中刚好是词汇表)。在PyTorch卷积实现中,输入通道的数量是in_channels参数。卷积操作可以在输出(out_channels)中产生多个通道。您可以将其视为卷积运算符将输入特征维“映射”到输出特征维。

卷积运算用两个输入矩阵(两个输入通道)表示相应的核也有两层,它将每层分别相乘,然后对结果求和。参数配置:input_channels=2, output_channels=1, kernel_size=2, tride=1, padding=0, and dilation=1
一种具有一个输入矩阵(一个输入通道)和两个卷积的卷积运算核(两个输出通道)。这些核分别应用于输入矩阵,并堆叠在输出张量。参数配置:input_channels=1, output_channels=2, kernel_size=2, tride=1, padding=0, and dilation=1

很难立即知道有多少输出通道适合当前的问题。为了简化这个困难,我们假设边界是1,1,024——我们可以有一个只有一个通道的卷积层,也可以有一个只有1,024个通道的卷积层。现在我们有了边界,接下来要考虑的是有多少个输入通道。一种常见的设计模式是,从一个卷积层到下一个卷积层,通道数量的缩减不超过2倍。这不是一个硬性的规则,但是它应该让您了解适当数量的out_channels是什么样子的。 

3.Stride(步长)

Stride控制卷积之间的步长。如果步长与核相同,则内核计算不会重叠。另一方面,如果跨度为1,则内核重叠最大。输出张量可以通过增加步幅的方式被有意的压缩来总结信息。

应用于具有超参数步长的输入的kernel_size=2的卷积核等于2。这会导致内核采取更大的步骤,从而产生更小的输出矩阵。对于更稀疏地对输入矩阵进行二次采样非常有用。

4.Padding(填充层) 

即使stride和kernel_size允许控制每个计算出的特征值有多大范围,它们也有一个有害的、有时是无意的副作用,那就是缩小特征映射的总大小(卷积的输出)。为了抵消这一点,输入数据张量被人为地增加了长度(如果是一维、二维或三维)、高度(如果是二维或三维)和深度(如果是三维),方法是在每个维度上附加和前置0。这意味着CNN将执行更多的卷积,但是输出形状可以控制,而不会影响所需的核大小、步幅或扩展。

应用于高度和宽度等于的输入矩阵的kernel_size=2的卷积2。但是,由于填充(用深灰色正方形表示),输入矩阵的高度和宽度可以被放大。这通常与大小为3的内核一起使用,这样输出矩阵将等于输入矩阵的大小。

5.Pooling(池化层)

Pooling是将高维特征映射总结为低维特征映射的操作。卷积的输出是一个特征映射。feature map中的值总结了输入的一些区域。由于卷积计算的重叠性,许多计算出的特征可能是冗余的。Pooling是一种将高维(可能是冗余的)特征映射总结为低维特征映射的方法。在形式上,池是一种像sum、mean或max这样的算术运算符,系统地应用于feature map中的局部区域,得到的池操作分别称为sum pooling、average pooling和max pooling。池还可以作为一种方法,将较大但较弱的feature map的统计强度改进为较小但较强的feature map。

这里所示的池操作在功能上与卷积相同:它应用于输入矩阵中的不同位置。然而,池操作不是将输入矩阵的值相乘和求和,而是应用一些函数G来汇集这些值。G可以是任何运算,但求和、求最大值和计算平均值是最常见的。

6.Dilation(膨胀/空洞卷积)

膨胀控制卷积核如何应用于输入矩阵。将膨胀从1(默认值)增加到2意味着当应用于输入矩阵时,核的元素彼此之间是两个空格。另一种考虑这个问题的方法是在核中跨跃——在核中的元素或核的应用之间存在一个step size,即存在“holes”。这对于在不增加参数数量的情况下总结输入空间的更大区域是有用的。当卷积层被叠加时,扩张卷积被证明是非常有用的。连续扩张的卷积指数级地增大了“接受域”的大小;即网络在做出预测之前所看到的输入空间的大小。

应用于超参数dilation=2的输入矩阵的kernel_size=2的卷积。从默认值开始膨胀的增加意味着核矩阵的元素在与输入矩阵相乘时进一步分散开来。进一步增大扩张会加剧这种扩散。

7.torch代码示例

本次实验一些基本术语:

batch_size——每次训练使用的样本数

one-hot_size——向量的维度,input channels的数量

sequence_width——序列长度,width

import torch
import torch.nn as nn
# 批大小
batch_size = 2
# one-hot 向量的维度(通道数)
one_hot_size = 10
# 序列长度
sequence_width = 7
# 创建一个随机的输入数据张量,形状为 (batch_size, one_hot_size, sequence_width)
data = torch.randn(batch_size, one_hot_size, sequence_width)
# 定义一个一维卷积层,输入通道数为 one_hot_size,输出通道数为 16,卷积核大小为 3
conv1 = nn.Conv1d(in_channels=one_hot_size, out_channels=16, kernel_size=3)
# 将输入数据通过卷积层进行前向传播
intermediate1 = conv1(data)
# 打印输入数据的形状
print(data.size())
# 打印卷积层输出数据的形状
print(intermediate1.size())

输出:
torch.Size([2, 10, 7])
torch.Size([2, 16, 5])

import torch
import torch.nn as nn

# 批大小
batch_size = 2

# one-hot 向量的维度(通道数)
one_hot_size = 10

# 序列长度
sequence_width = 7

# 创建一个随机的输入数据张量,形状为 (batch_size, one_hot_size, sequence_width)
data = torch.randn(batch_size, one_hot_size, sequence_width)

# 定义第一个一维卷积层,输入通道数为 one_hot_size,输出通道数为 16,卷积核大小为 3
conv1 = nn.Conv1d(in_channels=one_hot_size, out_channels=16, kernel_size=3)

# 将输入数据通过第一个卷积层进行前向传播
intermediate1 = conv1(data)

# 定义第二个一维卷积层,输入通道数为 16,输出通道数为 32,卷积核大小为 3
conv2 = nn.Conv1d(in_channels=16, out_channels=32, kernel_size=3)

# 定义第三个一维卷积层,输入通道数为 32,输出通道数为 64,卷积核大小为 3
conv3 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3)

# 将第一个卷积层的输出通过第二个卷积层进行前向传播
intermediate2 = conv2(intermediate1)

# 将第二个卷积层的输出通过第三个卷积层进行前向传播
intermediate3 = conv3(intermediate2)

# 打印第二个卷积层输出数据的形状
print(intermediate2.size())

# 打印第三个卷积层输出数据的形状
print(intermediate3.size())

输出:

torch.Size([2, 32, 3])

torch.Size([2, 64, 1])

三、CNN处理姓氏分类

1.数据集处理

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

我们使用数据集中最长的姓氏来控制onehot矩阵的大小有两个原因。首先,将每一小批姓氏矩阵组合成一个三维张量,要求它们的大小相同。其次,使用数据集中最长的姓氏意味着可以以相同的方式处理每个小批处理。

from torch.utils.data import Dataset

class SurnameDataset(Dataset):

    def __getitem__(self, index):
        # 从目标数据帧中获取第 index 行数据
        row = self._target_df.iloc[index]

        # 使用向量器将姓氏转换为向量矩阵,长度为最大序列长度
        surname_matrix = self._vectorizer.vectorize(row.surname, self._max_seq_length)

        # 查找对应的国籍索引
        nationality_index = self._vectorizer.nationality_vocab.lookup_token(row.nationality)

        # 返回包含姓氏矩阵和国籍索引的字典
        return {'x_surname': surname_matrix,
                'y_nationality': nationality_index}

2.向量化处理

在本例中,尽管词汇表和DataLoader的实现方式与“示例:带有多层感知器的姓氏分类”中的示例相同,但Vectorizer的vectorize()方法已经更改,以适应CNN模型的需要。具体来说,该函数将字符串中的每个字符映射到一个整数,然后使用该整数构造一个由onehot向量组成的矩阵。重要的是,矩阵中的每一列都是不同的onehot向量。主要原因是,我们将使用的Conv1d层要求数据张量在第0维上具有批处理,在第1维上具有通道,在第2维上具有特性。

除了更改为使用onehot矩阵之外,我们还修改了矢量化器,以便计算姓氏的最大长度并将其保存为max_surname_length

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

    def __init__(self, character_vocab, nationality_vocab, max_surname_length):
        """
        Args:
            character_vocab (Vocabulary): 处理字符的词汇表
            nationality_vocab (Vocabulary): 处理国籍的词汇表
            max_surname_length (int): 最长姓氏的长度,用于矩阵大小
        """
        self.character_vocab = character_vocab
        self.nationality_vocab = nationality_vocab
        self.max_surname_length = max_surname_length

    def vectorize(self, surname):
        """
        将姓氏向量化为一个 one-hot 矩阵

        Args:
            surname (str): 输入的姓氏
        Returns:
            one_hot_matrix (np.ndarray): 一个 one-hot 向量矩阵
        """

        # 定义 one-hot 矩阵的大小:字符词汇表的大小 × 最大姓氏长度
        one_hot_matrix_size = (len(self.character_vocab), self.max_surname_length)
        one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)

        # 遍历姓氏中的每个字符,将对应位置设置为 1
        for position_index, character in enumerate(surname):
            character_index = self.character_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 的一个实例
        """
        character_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:
                character_vocab.add_token(letter)
            nationality_vocab.add_token(row.nationality)

        return cls(character_vocab, nationality_vocab, max_surname_length)

 3.构建模型

在本例中,我们将每个卷积的通道数与num_channels超参数绑定。我们可以选择不同数量的通道分别进行卷积运算。这样做需要优化更多的超参数。我们发现256足够大,可以使模型达到合理的性能。

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

class SurnameClassifier(nn.Module):
    def __init__(self, initial_num_channels, num_classes, num_channels):
        """
        初始化姓氏分类器

        Args:
            initial_num_channels (int): 输入特征向量的大小
            num_classes (int): 输出预测向量的大小
            num_channels (int): 网络中使用的常量通道大小
        """
        super(SurnameClassifier, self).__init__()
        # 使用四层卷积
        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)
        
        # 通过全连接层进行分类预测
        prediction_vector = self.fc(features)

        # 如果需要应用 softmax 激活,则进行 softmax 操作
        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)

        return prediction_vector

 通常为了获取更好的性能,一般的操作是增加卷积层数,本次模型使用了四层卷积,你可以尝试使用更深的网络,增加到5,6,7,8层。

同样,你可以更换网络架构,有很多方法可能会给你的结果带来更好的指标,比如使用Unet、DenseNet、ResNet等架构,这些网络框架已经取得成功,感兴趣可以查询资料学习。

 4.训练

训练集(Training Set): 用于训练模型的数据集。训练集用来训练模型,拟合出数据分布规律,即确定模型的权重和偏置等参数,这些参数称为学习参数。

def make_train_state(args):
    """
    创建训练状态字典。

    Args:
        args (Namespace): 包含训练参数的命名空间对象。

    Returns:
        dict: 训练状态字典。
    """
    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}  # 模型文件名
import torch

def update_train_state(args, model, train_state):
    """
    处理训练状态更新。

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

    Args:
        args: 主要参数
        model: 要训练的模型
        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

    # 如果训练过程中的 epoch 数大于等于 1
    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):
    """
    计算预测的准确率。

    Args:
        y_pred (torch.Tensor): 模型的预测结果张量。
        y_target (torch.Tensor): 真实标签张量。

    Returns:
        float: 准确率,以百分比表示。
    """
    # 获取预测的类别索引
    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(
    # Data and Path information
    surname_csv="data/surnames/surnames_with_splits.csv",
    vectorizer_file="vectorizer.json",
    model_state_file="model.pth",
    save_dir="model_storage/ch4/cnn",
    # Model hyper parameters
    hidden_dim=100,
    num_channels=256,
    # Training hyper parameters
    seed=1337,
    learning_rate=0.001,
    batch_size=128,
    num_epochs=100,
    early_stopping_criteria=5,
    dropout_p=0.1,
    # Runtime options
    cuda=False,
    reload_from_files=False,
    expand_filepaths_to_save_dir=True,
    catch_keyboard_interrupt=True
)


if args.expand_filepaths_to_save_dir:
    # 如果设置了 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))

    
# Check 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 for reproducibility
set_seed_everywhere(args.seed, args.cuda)

# handle dirs
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:
    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")

 训练过程:

5.评估 

在训练完成后,我们要进行评估,通常在这个环节我们是用于选择模型最好的参数以优化模型性能。

验证集(Validation Set): 用于验证模型性能的数据集。在模型训练过程中,验证集用来调整模型参数和超参数,以优化模型性能,避免过拟合,即验证集用于模型选择,并不参与学习参数的确定,而是为了选择出模型误差较小的模型参数和超参数。

# 加载模型状态字典
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):
    # compute the output
    y_pred =  classifier(batch_dict['x_surname'])
    
    # 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)

train_state['test_loss'] = running_loss
train_state['test_acc'] = running_acc

 6.预测

测试集(Test Set): 用于评估模型性能的数据集。在模型训练完成后,测试集用来评估模型的泛化能力(泛化能力即模型在未知数据上的表现),即测试集仅在训练完成后使用一次,评价最终模型的效果(其实,测试集可以跑多个epoch)。

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}
# 获取用户输入的新姓氏
new_surname = input("Enter a surname to classify: ")

# 将分类器移动到 CPU 上进行预测
classifier = classifier.cpu()

# 进行国籍预测
prediction = predict_nationality(new_surname, classifier, vectorizer)

# 打印预测结果,包括姓氏、国籍和概率
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

示例: 

def predict_topk_nationality(surname, classifier, vectorizer, k=5):
    """预测新姓氏的前 K 个国籍
    
    Args:
        surname (str): 要分类的姓氏
        classifier (SurnameClassifer): 分类器的实例
        vectorizer (SurnameVectorizer): 相应的向量化器
        k (int): 要返回的前 K 个国籍数量
    
    Returns:
        list: 包含字典的列表,每个字典包含一个国籍及其概率
    """
    # 向量化姓氏
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(dim=0)  
    # 使用分类器进行预测,并获取前 K 个概率值和对应的索引
    prediction_vector = classifier(vectorized_surname, apply_softmax=True)
    probability_values, indices = torch.topk(prediction_vector, k=k)    
    # 将张量转换为 NumPy 数组
    probability_values = probability_values[0].detach().numpy()
    indices = indices[0].detach().numpy()    
    results = []
    # 遍历前 K 个国籍,构建结果列表
    for kth_index in range(k):
        nationality = vectorizer.nationality_vocab.lookup_index(indices[kth_index])
        probability_value = probability_values[kth_index]
        results.append({'nationality': nationality, 
                        'probability': probability_value})
    return results
# 获取用户输入的新姓氏
new_surname = input("Enter a surname to classify: ")
# 获取用户想要查看的前 K 个预测数量
k = int(input("How many of the top predictions to see? "))
# 如果用户输入的 K 大于国籍数量,则使用最大国籍数量作为 K
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)
# 进行前 K 个国籍的预测
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']))

示例:


总结

现在的CNN具有更多的architecture,从VGG,GooleNet,ResNet一路发展以来包括DenseNet,SENet,CBAM这些架构都在努力尝试不同的方向从而提高模型性能,选取适合的网络框架可以让你的训练事半功倍,有时候就是多种模型的排列组合可以让你有一个满意的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值