当前位置:   article > 正文

NLP—基于MLP和CNN实现姓氏分类_mlp分类预测

mlp分类预测

目录

一、基于多层感知机(MLP)实现姓氏分类

1.1 MLP原理

1.2 激活函数

1.2.1 激活函数性质

1.2.2 sigmoid函数

1.2.3 tanh函数

1.3 前向传播

1.4 反向传播 

 1.5 MLP实现姓氏分类

1.5.1 数据集(The SurnameDataset)

1.5.2 词汇表、向量化器(Vocabulary, Vectorizer, and DataLoader)

1.5.3 分类模型(The Surname Classifier Model) 

 1.5.4 训练(The Training Routine)

 1.5.5 评估(Evaluation)

1.5.6 预测(Predict)

 二、基于卷积神经网络(CNN)实现姓氏分类

2.1 CNN基本结构

2.1.1 卷积层(Convolutional Layer)

2.1.2 激活层(Activation Layer)

2.1.3 池化层(Pooling Layer)

2.1.4 全连接层(Fully Connected Layer)

2.2 CNN主要参数

2.2.1 卷积核(Kernels)

2.2.2 步长(Stride)

2.2.3 填充(Padding)

2.2.4 通道数(Channels)

2.2.5 池化核(Pooling Kernels)

2.2.6 膨胀/空洞卷积(Dilated Convolution)

2.3 CNN实现姓氏分类

2.3.1  数据集(The SurnameDataset)

2.3.2 词汇表、向量化器(Vocabulary, Vectorizer, and DataLoader)

2.3.3 分类模型(The SurnameClassifier)

2.3.4 训练(The Training Routine)

2.3.5 评估(Evaluation)

2.3.6 预测(Prediction)


实验前言

本篇文章一共进行两个实验,分别是基于MLP实现姓氏分类和基于CNN实现姓氏分类,因篇幅原因,数据集和代码部分未完整给出,有想要完整代码和数据集的读友,可以评论区留言或后台私A信。另外要感谢董老师提供的代码、资料以及指导。

本人水平有限,希望各位读友点点小赞。

实验环境

  • Python 3.6.7
  • torch框架

一、基于多层感知机(MLP)实现姓氏分类

1.1 MLP原理

多层感知机(Multilayer Perceptron,MLP)是一种基本的人工神经网络模型,通常用于解决分类和回归问题。它由多个神经元层组成,每一层都与下一层全连接,信息通过网络从输入层传递到输出层。每个神经元接收上一层的输出,并将其加权和传递给下一层,最终得到输出结果。

MLP的每个神经元都包含一个激活函数,通常是非线性的,以使得网络可以学习非线性模式。常见的激活函数包括Sigmoid、Tanh等。MLP通过反向传播算法来训练,该算法使用梯度下降来调整网络中的权重,以最小化损失函数。通过反复迭代训练数据,MLP可以逐渐调整其参数,使其能够更好地拟合训练数据,并在未见过的数据上进行泛化。

MLP是一种灵活且强大的模型,可以应用于各种问题领域,如图像识别、自然语言处理、推荐系统等。

1.2 激活函数

1.2.1 激活函数性质

激活函数需要具备以下几点性质:

        1. 连续并可导(允许少数点上不可导)的非线性函数。可导的激活函数可以直接利用数值优化的方法来学习网络参数。
        2. 激活函数及其导函数要尽可能的简单,有利于提高网络计算效率。
        3. 激活函数的导函数的值域要在一个合适的区间内,不能太大也不能太小,否则会影响训练的效率和稳定性。 

1.2.2 sigmoid函数

sigmoid 是神经网络历史上最早使用的激活函数之一。它取任何实值并将其压缩在0和1之间。数学上,sigmoid 的表达式如下:

y=\frac{1}{1+e^{-x}}

从结果中很容易看出,sigmoid 是一个光滑的、可微的函数。

1.2.3 tanh函数

tanh 激活函数是 sigmoid 在外观上的不同变体,输出范围在(-1, 1)之间,输出值为零中心,这意味着数据的平均值更接近于零,有助于加快收敛速度,tanh的表达式如下:

y=\frac{e^{x}-e^{-x}}{e^{x}+e^{-x}}

tanh在特征相差明显时的效果会很好,在循环过程中会不断扩大特征效果。与sigmod的区别是 tanh 是0 的均值,因此在实际应用中tanh会比sigmod更好。

在具体应用中,tanh函数相比于Sigmoid函数往往更具有优越性,这主要是因为Sigmoid函数在输入处于[-1,1]之间时,函数值变化敏感,一旦接近或者超出区间就失去敏感性,处于饱和状态。


1.3 前向传播

MLP的前向传播过程即从输入层到输出层的计算过程。它涉及到权重和偏置的计算、激活函数的应用等。具体步骤如下:

假设网络有L层,其中第l层的输入a^{(l-1)}为,权重矩阵为W^{(l)},偏置向量为b^{(l)},激活函数为f,则第l层的输出a^{(l)}计算如下:

z^{(l)}=W^{(l)}a^{(l-1)}+b^{(l)}

a^{(l)}=f(z^{(l)})


1.4 反向传播 

反向传播算法是用于训练MLP模型的关键步骤。通过计算梯度来调整权重和偏置,以最小化预测结果与真实结果之间的误差。具体步骤如下:

  • 损失计算:首先计算网络的输出与真实标签之间的损失。常用的损失函数包括均方误差(MSE)和交叉熵(Cross-Entropy)。

  • 计算输出层的梯度

    • 损失函数对输出层的输入z^{(l)}的梯度:

\delta ^{(l)}=\frac{\partial L}{\partial z^{(l)}}

  • 计算隐藏层的梯度

    • 损失函数对第l层输入的梯度z^{(l)}

\delta ^{l}=(W^{(l+1)})^{T}\delta ^{(l+1)}\circ f'(z^{(l)})​​​​​​​

              其中,\circ 表示元素乘积(Hadamard乘积),f'(z^{(l)})是激活函数的导数。

  • 计算梯度

    • 对于权重W^{(l)}和偏置 b^{(l)}的梯度:

\frac{\partial L}{\partial W^{(l)}}=\delta ^{(l)}(a^{(l-1)})^{T}

\frac{\partial L}{\partial b^{(l)}}=\delta ^{(l)}

  • 更新权重和偏置

    • 使用梯度下降法更新参数:

W^{(l)}=W^{(l)}-\eta \frac{\partial L}{\partial W^{(l)}}

b^{(l)}=b^{(l)}-\eta \frac{\partial L}{\partial b^{(l)}}

           其中,\eta是学习率。


 1.5 MLP实现姓氏分类

1.5.1 数据集(The SurnameDataset)

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

为了创建最终的数据集,从一个比课程补充材料中包含的版本处理更少的版本开始,并执行了几个数据集修改操作。第一个目的是减少这种不平衡——原始数据集中70%以上是俄文,这可能是由于抽样偏差或俄文姓氏的增多。为此,我们通过选择标记为俄语的姓氏的随机子集对这个过度代表的类进行子样本。接下来,我们根据国籍对数据集进行分组,并将数据集分为三个部分:70%到训练数据集,15%到验证数据集,最后15%到测试数据集,以便跨这些部分的类标签分布具有可比性。

代码如下:

  1. class SurnameDataset(Dataset):
  2. def __init__(self, surname_df, vectorizer):
  3. """
  4. 参数:
  5. surname_df (pandas.DataFrame): 数据集
  6. vectorizer (SurnameVectorizer): 从数据集实例化的向量化器
  7. """
  8. self.surname_df = surname_df
  9. self._vectorizer = vectorizer
  10. # 拆分数据集为训练集、验证集和测试集
  11. self.train_df = self.surname_df[self.surname_df.split=='train']
  12. self.train_size = len(self.train_df)
  13. self.val_df = self.surname_df[self.surname_df.split=='val']
  14. self.validation_size = len(self.val_df)
  15. self.test_df = self.surname_df[self.surname_df.split=='test']
  16. self.test_size = len(self.test_df)
  17. self._lookup_dict = {'train': (self.train_df, self.train_size),
  18. 'val': (self.val_df, self.validation_size),
  19. 'test': (self.test_df, self.test_size)}
  20. self.set_split('train')
  21. # 类别权重
  22. class_counts = surname_df.nationality.value_counts().to_dict()
  23. def sort_key(item):
  24. return self._vectorizer.nationality_vocab.lookup_token(item[0])
  25. sorted_counts = sorted(class_counts.items(), key=sort_key)
  26. frequencies = [count for _, count in sorted_counts]
  27. self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)
  28. @classmethod
  29. def load_dataset_and_make_vectorizer(cls, surname_csv):
  30. """加载数据集并从头创建一个新的向量化器
  31. 参数:
  32. surname_csv (str): 数据集的位置
  33. 返回:
  34. SurnameDataset的一个实例
  35. """
  36. surname_df = pd.read_csv(surname_csv)
  37. train_surname_df = surname_df[surname_df.split=='train']
  38. return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
  39. @classmethod
  40. def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
  41. """加载数据集和相应的向量化器。用于向量化器已被缓存以供重用的情况
  42. 参数:
  43. surname_csv (str): 数据集的位置
  44. vectorizer_filepath (str): 已保存的向量化器的位置
  45. 返回:
  46. SurnameDataset的一个实例
  47. """
  48. surname_df = pd.read_csv(surname_csv)
  49. vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
  50. return cls(surname_df, vectorizer)
  51. @staticmethod
  52. def load_vectorizer_only(vectorizer_filepath):
  53. """从文件加载向量化器的静态方法
  54. 参数:
  55. vectorizer_filepath (str): 序列化向量化器的位置
  56. 返回:
  57. SurnameVectorizer的一个实例
  58. """
  59. with open(vectorizer_filepath) as fp:
  60. return SurnameVectorizer.from_serializable(json.load(fp))
  61. def save_vectorizer(self, vectorizer_filepath):
  62. """使用json将向量化器保存到磁盘
  63. 参数:
  64. vectorizer_filepath (str): 保存向量化器的位置
  65. """
  66. with open(vectorizer_filepath, "w") as fp:
  67. json.dump(self._vectorizer.to_serializable(), fp)
  68. def get_vectorizer(self):
  69. """ 返回向量化器 """
  70. return self._vectorizer
  71. def set_split(self, split="train"):
  72. """ 使用数据框中的列选择数据集的拆分 """
  73. self._target_split = split
  74. self._target_df, self._target_size = self._lookup_dict[split]
  75. def __len__(self):
  76. return self._target_size
  77. def __getitem__(self, index):
  78. """PyTorch数据集的主要入口点方法
  79. 参数:
  80. index (int): 数据点的索引
  81. 返回:
  82. 一个包含数据点的字典:
  83. 特征 (x_surname)
  84. 标签 (y_nationality)
  85. """
  86. row = self._target_df.iloc[index]
  87. surname_vector = self._vectorizer.vectorize(row.surname)
  88. nationality_index = self._vectorizer.nationality_vocab.lookup_token(row.nationality)
  89. return {'x_surname': surname_vector,
  90. 'y_nationality': nationality_index}
  91. def get_num_batches(self, batch_size):
  92. """给定批量大小,返回数据集中的批次数量
  93. 参数:
  94. batch_size (int)
  95. 返回:
  96. 数据集中的批次数量
  97. """
  98. return len(self) // batch_size
  99. def generate_batches(dataset, batch_size, shuffle=True,
  100. drop_last=True, device="cpu"):
  101. """
  102. 一个包装PyTorch DataLoader的生成器函数。它将确保每个张量在正确的设备位置上。
  103. """
  104. dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
  105. shuffle=shuffle, drop_last=drop_last)
  106. for data_dict in dataloader:
  107. out_data_dict = {}
  108. for name, tensor in data_dict.items():
  109. out_data_dict[name] = data_dict[name].to(device)
  110. yield out_data_dict

1.5.2 词汇表、向量化器(Vocabulary, Vectorizer, and DataLoader)

  1. class Vocabulary(object):
  2. """用于处理文本并提取词汇以进行映射的类"""
  3. def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
  4. """
  5. 初始化Vocabulary实例。
  6. 参数:
  7. token_to_idx (dict): 一个现有的将标记映射到索引的字典
  8. add_unk (bool): 一个指示是否添加UNK标记的标志
  9. unk_token (str): 要添加到词汇表中的UNK标记
  10. """
  11. if token_to_idx is None:
  12. token_to_idx = {}
  13. self._token_to_idx = token_to_idx
  14. # 创建从索引到标记的反向映射
  15. self._idx_to_token = {idx: token for token, idx in self._token_to_idx.items()}
  16. self._add_unk = add_unk
  17. self._unk_token = unk_token
  18. self.unk_index = -1
  19. # 如果要求,将UNK标记添加到词汇表中
  20. if add_unk:
  21. self.unk_index = self.add_token(unk_token)
  22. def to_serializable(self):
  23. """返回一个可序列化的字典。"""
  24. return {'token_to_idx': self._token_to_idx,
  25. 'add_unk': self._add_unk,
  26. 'unk_token': self._unk_token}
  27. @classmethod
  28. def from_serializable(cls, contents):
  29. """从序列化字典实例化Vocabulary。"""
  30. return cls(**contents)
  31. def add_token(self, token):
  32. """基于标记更新映射字典。
  33. 参数:
  34. token (str): 要添加到Vocabulary中的项
  35. 返回:
  36. index (int): 对应于标记的整数
  37. """
  38. try:
  39. index = self._token_to_idx[token]
  40. except KeyError:
  41. index = len(self._token_to_idx)
  42. self._token_to_idx[token] = index
  43. self._idx_to_token[index] = token
  44. return index
  45. def add_many(self, tokens):
  46. """将标记列表添加到Vocabulary中。
  47. 参数:
  48. tokens (list): 字符串标记列表
  49. 返回:
  50. indices (list): 与标记对应的索引列表
  51. """
  52. return [self.add_token(token) for token in tokens]
  53. def lookup_token(self, token):
  54. """检索与标记相关联的索引,如果标记不存在,则使用UNK索引。
  55. 参数:
  56. token (str): 要查找的标记
  57. 返回:
  58. index (int): 与标记相关的索引
  59. 注意:
  60. UNK功能需要unk_index >= 0(已添加到Vocabulary中)
  61. """
  62. if self.unk_index >= 0:
  63. return self._token_to_idx.get(token, self.unk_index)
  64. else:
  65. return self._token_to_idx[token]
  66. def lookup_index(self, index):
  67. """返回与索引相关联的标记。
  68. 参数:
  69. index (int): 要查找的索引
  70. 返回:
  71. token (str): 与索引相关的标记
  72. 引发:
  73. KeyError: 如果索引不在Vocabulary中
  74. """
  75. if index not in self._idx_to_token:
  76. raise KeyError("索引(%d)不在Vocabulary中" % index)
  77. return self._idx_to_token[index]
  78. def __str__(self):
  79. """返回Vocabulary的字符串表示形式。"""
  80. return "<Vocabulary(size=%d)>" % len(self)
  81. def __len__(self):
  82. """返回Vocabulary中唯一标记的数量。"""
  83. return len(self._token_to_idx)
  1. class SurnameVectorizer(object):
  2. """协调Vocabularies并将它们应用于实际用途的向量化器"""
  3. def __init__(self, surname_vocab, nationality_vocab):
  4. """
  5. 参数:
  6. surname_vocab (Vocabulary): 将字符映射到整数的词汇表
  7. nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
  8. """
  9. self.surname_vocab = surname_vocab
  10. self.nationality_vocab = nationality_vocab
  11. def vectorize(self, surname):
  12. """
  13. 将姓氏向量化为一种折叠的one-hot编码。
  14. 参数:
  15. surname (str): 姓氏
  16. 返回:
  17. one_hot (np.ndarray): 一个折叠的one-hot编码
  18. """
  19. vocab = self.surname_vocab
  20. one_hot = np.zeros(len(vocab), dtype=np.float32)
  21. for token in surname:
  22. one_hot[vocab.lookup_token(token)] = 1
  23. return one_hot
  24. @classmethod
  25. def from_dataframe(cls, surname_df):
  26. """从数据集DataFrame实例化向量化器。
  27. 参数:
  28. surname_df (pandas.DataFrame): 姓氏数据集
  29. 返回:
  30. SurnameVectorizer的实例
  31. """
  32. surname_vocab = Vocabulary(unk_token="@")
  33. nationality_vocab = Vocabulary(add_unk=False)
  34. for index, row in surname_df.iterrows():
  35. for letter in row.surname:
  36. surname_vocab.add_token(letter)
  37. nationality_vocab.add_token(row.nationality)
  38. return cls(surname_vocab, nationality_vocab)
  39. @classmethod
  40. def from_serializable(cls, contents):
  41. surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
  42. nationality_vocab = Vocabulary.from_serializable(contents['nationality_vocab'])
  43. return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab)
  44. def to_serializable(self):
  45. """返回一个可序列化的字典。"""
  46. return {'surname_vocab': self.surname_vocab.to_serializable(),
  47. 'nationality_vocab': self.nationality_vocab.to_serializable()}

1.5.3 分类模型(The Surname Classifier Model) 

第一个线性层将输入向量映射到中间向量,并对该向量应用非线性。第二线性层将中间向量映射到预测向量。在最后一步中,可选地应用softmax操作,以确保输出和为1;这就是所谓的“概率”。它是可选的原因与我们使用的损失函数的数学公式有关——交叉熵损失。我们研究了“损失函数”中的交叉熵损失。回想一下,交叉熵损失对于多类分类是最理想的,但是在训练过程中软最大值的计算不仅浪费而且在很多情况下并不稳定。

代码如下:

  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. class SurnameClassifier(nn.Module):
  4. """用于姓氏分类的两层多层感知器"""
  5. def __init__(self, input_dim, hidden_dim, output_dim):
  6. """
  7. 参数:
  8. input_dim (int): 输入向量的大小
  9. hidden_dim (int): 第一个线性层的输出大小
  10. output_dim (int): 第二个线性层的输出大小
  11. """
  12. super(SurnameClassifier, self).__init__()
  13. self.fc1 = nn.Linear(input_dim, hidden_dim) # 第一个线性层
  14. self.fc2 = nn.Linear(hidden_dim, output_dim) # 第二个线性层
  15. def forward(self, x_in, apply_softmax=False):
  16. """分类器的前向传播
  17. 参数:
  18. x_in (torch.Tensor): 输入数据张量。x_in的形状应为 (batch, input_dim)
  19. apply_softmax (bool): 是否应用softmax激活的标志
  20. 如果与交叉熵损失一起使用,应设置为False
  21. 返回:
  22. 结果张量。张量的形状应为 (batch, output_dim)
  23. """
  24. intermediate_vector = F.relu(self.fc1(x_in)) # 通过第一个线性层并应用ReLU激活函数
  25. prediction_vector = self.fc2(intermediate_vector) # 通过第二个线性层
  26. if apply_softmax:
  27. prediction_vector = F.softmax(prediction_vector, dim=1) # 如果需要,应用softmax激活函数
  28. return prediction_vector

 1.5.4 训练(The Training Routine

虽然我们使用了不同的模型、数据集和损失函数,但是训练例程是相同的。

部分代码如下:

  1. # 设置参数
  2. args = Namespace(
  3. # 数据和路径信息
  4. surname_csv="data/surnames/surnames_with_splits.csv",
  5. vectorizer_file="vectorizer.json",
  6. model_state_file="model.pth",
  7. save_dir="model_storage/ch4/surname_mlp",
  8. # 模型超参数
  9. hidden_dim=300,
  10. # 训练超参数
  11. seed=1337,
  12. num_epochs=100,
  13. early_stopping_criteria=5,
  14. learning_rate=0.001,
  15. batch_size=64,
  16. # 运行时选项
  17. cuda=False,
  18. reload_from_files=False,
  19. expand_filepaths_to_save_dir=True,
  20. )
  21. # 扩展文件路径
  22. if args.expand_filepaths_to_save_dir:
  23. args.vectorizer_file = os.path.join(args.save_dir, args.vectorizer_file)
  24. args.model_state_file = os.path.join(args.save_dir, args.model_state_file)
  25. print("扩展的文件路径: ")
  26. print("\t{}".format(args.vectorizer_file))
  27. print("\t{}".format(args.model_state_file))
  28. # 检查 CUDA 可用性
  29. if not torch.cuda.is_available():
  30. args.cuda = False
  31. args.device = torch.device("cuda" if args.cuda else "cpu")
  32. print("使用 CUDA: {}".format(args.cuda))
  33. # 设置随机种子以保证可重复性
  34. def set_seed_everywhere(seed, cuda):
  35. torch.manual_seed(seed)
  36. if cuda:
  37. torch.cuda.manual_seed_all(seed)
  38. set_seed_everywhere(args.seed, args.cuda)
  39. # 处理目录
  40. def handle_dirs(dirpath):
  41. if not os.path.exists(dirpath):
  42. os.makedirs(dirpath)
  43. handle_dirs(args.save_dir)

训练中最显著的差异与模型中输出的种类和使用的损失函数有关。在这个例子中,输出是一个多类预测向量,可以转换为概率。正如在模型描述中所描述的,这种输出的损失类型仅限于CrossEntropyLoss和NLLLoss。由于它的简化,我们使用了CrossEntropyLoss。

THE TRAINING LOOP

示例显示了使用不同的key从batch_dict中获取数据。除了外观上的差异,训练循环的功能保持不变。利用训练数据,计算模型输出、损失和梯度。然后,使用梯度来更新模型。 

  1. scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
  2. mode='min', factor=0.5,
  3. patience=1)
  4. # 初始化训练状态
  5. train_state = make_train_state(args)
  6. epoch_bar = tqdm(desc='training routine', # 迭代轮次
  7. total=args.num_epochs,
  8. position=0)
  9. dataset.set_split('train')
  10. train_bar = tqdm(desc='split=train',
  11. total=dataset.get_num_batches(args.batch_size),
  12. position=1,
  13. leave=True)
  14. dataset.set_split('val')
  15. val_bar = tqdm(desc='split=val',
  16. total=dataset.get_num_batches(args.batch_size),
  17. position=1,
  18. leave=True)
  19. try:
  20. for epoch_index in range(args.num_epochs):
  21. train_state['epoch_index'] = epoch_index
  22. # Iterate over training dataset
  23. # 设置: 批处理生成器, 将损失和准确率设为0, 设置训练模式
  24. dataset.set_split('train')
  25. batch_generator = generate_batches(dataset,
  26. batch_size=args.batch_size,
  27. device=args.device)
  28. running_loss = 0.0
  29. running_acc = 0.0
  30. classifier.train()
  31. for batch_index, batch_dict in enumerate(batch_generator):
  32. # the training routine is these 5 steps:
  33. # --------------------------------------
  34. # 步骤 1. 清零梯度
  35. optimizer.zero_grad()
  36. # 计算输出
  37. y_pred = classifier(batch_dict['x_surname'])
  38. # 计算损失
  39. loss = loss_func(y_pred, batch_dict['y_nationality'])
  40. loss_t = loss.item()
  41. running_loss += (loss_t - running_loss) / (batch_index + 1)
  42. # 计算准确率
  43. loss.backward()
  44. # step 5. use optimizer to take gradient step
  45. optimizer.step()
  46. # -----------------------------------------
  47. # compute the accuracy
  48. acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
  49. running_acc += (acc_t - running_acc) / (batch_index + 1)
  50. # update bar
  51. train_bar.set_postfix(loss=running_loss, acc=running_acc,
  52. epoch=epoch_index)
  53. train_bar.update()
  54. train_state['train_loss'].append(running_loss)
  55. train_state['train_acc'].append(running_acc)
  56. # Iterate over val dataset
  57. # setup: batch generator, set loss and acc to 0; set eval mode on
  58. dataset.set_split('val')
  59. batch_generator = generate_batches(dataset,
  60. batch_size=args.batch_size,
  61. device=args.device)
  62. running_loss = 0.
  63. running_acc = 0.
  64. classifier.eval()
  65. for batch_index, batch_dict in enumerate(batch_generator):
  66. # 计算输出
  67. y_pred = classifier(batch_dict['x_surname'])
  68. # 计算损失
  69. loss = loss_func(y_pred, batch_dict['y_nationality'])
  70. loss_t = loss.to("cpu").item()
  71. running_loss += (loss_t - running_loss) / (batch_index + 1)
  72. # 计算准确率
  73. acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
  74. running_acc += (acc_t - running_acc) / (batch_index + 1)
  75. val_bar.set_postfix(loss=running_loss, acc=running_acc,
  76. epoch=epoch_index)
  77. val_bar.update()
  78. train_state['val_loss'].append(running_loss)
  79. train_state['val_acc'].append(running_acc)
  80. # 更新训练状态并根据验证损失调整学习率
  81. train_state = update_train_state(args=args, model=classifier,
  82. train_state=train_state)
  83. scheduler.step(train_state['val_loss'][-1])
  84. if train_state['stop_early']:
  85. break
  86. train_bar.n = 0
  87. val_bar.n = 0
  88. epoch_bar.update()
  89. except KeyboardInterrupt:
  90. print("Exiting loop")

 1.5.5 评估(Evaluation)

为了要了解模型的性能,需要对性能进行定量和定性的度量,现在通过以下代码,打印损失和准确率。

  1. # 加载最佳模型
  2. classifier.load_state_dict(torch.load(train_state['model_filename']))
  3. # 将模型和数据集转移到GPU或CPU
  4. classifier = classifier.to(args.device)
  5. dataset.class_weights = dataset.class_weights.to(args.device)
  6. # 定义损失函数
  7. loss_func = nn.CrossEntropyLoss(dataset.class_weights)
  8. # 设置数据集为测试集并迭代批次计算损失和准确率
  9. dataset.set_split('test')
  10. batch_generator = generate_batches(dataset,
  11. batch_size=args.batch_size,
  12. device=args.device)
  13. running_loss = 0.
  14. running_acc = 0.
  15. classifier.eval()
  16. for batch_index, batch_dict in enumerate(batch_generator):
  17. # 计算输出
  18. y_pred = classifier(batch_dict['x_surname'])
  19. # 计算损失
  20. loss = loss_func(y_pred, batch_dict['y_nationality'])
  21. loss_t = loss.item()
  22. running_loss += (loss_t - running_loss) / (batch_index + 1)
  23. # 计算准确率
  24. acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
  25. running_acc += (acc_t - running_acc) / (batch_index + 1)
  26. # 更新训练状态的测试损失和准确率
  27. train_state['test_loss'] = running_loss
  28. train_state['test_acc'] = running_acc
  29. # 打印测试损失和准确率
  30. print("Test loss: {};".format(train_state['test_loss']))
  31. print("Test Accuracy: {}".format(train_state['test_acc']))

​​​​​结果如下:

​​​​​​​​​​​​​​

由结果可知,MLP的准确率为46.69%。


1.5.6 预测(Predict)

代码如下:

  1. def predict_nationality(surname, classifier, vectorizer):
  2. """预测姓氏的国籍
  3. Args:
  4. surname (str): 要分类的姓氏
  5. classifier (SurnameClassifer): 分类器的实例
  6. vectorizer (SurnameVectorizer): 对应的向量化器
  7. Returns:
  8. 包含最可能的国籍及其概率的字典
  9. """
  10. # 将姓氏向量化
  11. vectorized_surname = vectorizer.vectorize(surname)
  12. vectorized_surname = torch.tensor(vectorized_surname).view(1, -1)
  13. # 使用分类器进行预测,并应用softmax函数
  14. result = classifier(vectorized_surname, apply_softmax=True)
  15. # 获取概率值和索引
  16. probability_values, indices = result.max(dim=1)
  17. index = indices.item()
  18. # 查找国籍
  19. predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
  20. probability_value = probability_values.item()
  21. return {'nationality': predicted_nationality, 'probability': probability_value}
  22. # 输入要分类的姓氏
  23. new_surname = input("Enter a surname to classify: ")
  24. # 将分类器移到CPU上
  25. classifier = classifier.to("cpu")
  26. # 使用预测函数进行预测
  27. prediction = predict_nationality(new_surname, classifier, vectorizer)
  28. # 打印结果
  29. print("{} -> {} (p={:0.2f})".format(new_surname,
  30. prediction['nationality'],
  31. prediction['probability']))

预测结果如下:

作者小王输入自己的姓氏“王”,由结果可知,预测出最可能的国籍是韩国,预测大概出错了(声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】

推荐阅读
相关标签