当前位置:   article > 正文

NLP——使用前馈神经网络进行姓氏分类

NLP——使用前馈神经网络进行姓氏分类

目录

一、实验背景

二、实验内容与环境

2.1 任务

2.2 实验环境

三、多层感知机(The Multilayer Perceptron)

3.1 MLP介绍

3.2 在PyTorch中的实现

3.2.1 实例化MLP

 3.2.2 模型完整性检查

 ​编辑

3.3 姓氏分类任务(基于MLP)

3.3.1 数据集预处理

3.3.2  Training Routine(训练例程)

3.3.3 模型测试

3.3.4 模型评估和预测

3.3.5 Dropout解决过拟合(结构正则化)

四、卷积神经网络(CNN)

4.1 CNN介绍

4.2 在pytorch中实现CNN

       4.2.1 减小张量的方法

4.2.2 数据处理函数

Vocabulary类

 SurnameVectorizer类

 SurnameDataset类:

 4.2.2 实例化CNN

五、分析总结

5.1 多层感知机在多层分类中的应用

 5.2 神经网络层对数据张量大小和形状的影响

 5.3 带有dropout的模型效果分析

总结


一、实验背景

       作为现存最简单的神经网络,感知器不能处理非线性可分的决策任务(比如图1.1 XOR数据集),这种情况下我们可以考虑两种前馈神经网络模型:多层感知器和卷积神经网络。多层感知器(MLP)被认为是最基本的神经网络构建模块之一;卷积神经网络,因其窗口特性能够在输入中学习局部化模式,这不仅使其成为计算机视觉的主轴,而且在检测单词和句子等序列数据中的子结构任务中效果很好。接下来会详细介绍MLP和卷积神经网络的结构及其在姓氏分类任务中的运用。

图1.1 非此即彼(XOR)数据集

二、实验内容与环境

2.1 任务

(1) 通过“示例:带有多层感知器的姓氏分类”,掌握多层感知器在多层分类中的应用

(2)掌握每种类型的神经网络层对它所计算的数据张量的大小和形状的影响

(3)尝试带有dropout的SurnameClassifier模型,看看它如何更改结果

(4)实验数据为姓氏数据集surnames.csv

2.2 实验环境

  • Python 3.6.7

三、多层感知机(The Multilayer Perceptron)

3.1 MLP介绍

       最简单的MLP由三层组成(如图3.1所示),分别为1.输入向量,负责接收输入特征;2.隐藏向量,位于输入层和输出层之间的中间层。每个隐藏层包含多个神经元(节点),隐藏层的输入即为输入层的输出,值是组成该层的不同感知器的输出;3.输出向量,产生最终输出,在分类任务中,每个神经元代表一个类别标签。

图3.1 最简单的MLP结构

       在MLP中,每个神经元类似于感知器,计算其输入的加权和,并应用激活函数以产生输出。一个层中神经元的输出作为下一层神经元的输入,通过网络传播信息。其中最常用的激活函数有sigmoid、tanh(双曲正切)和ReLU(修正线性单元),这些函数引入非线性,使MLP能够学习数据中的复杂关系。

       MLP的多层神经元(输入层、隐藏层和输出层),使其能够学习和表示数据中的非线性关系。它能够近似复杂的函数,学习不同抽象层次的特征。MLP使用激活函数并能够通过反向传播训练来调整权重和偏置,因此比单层感知器更适合处理复杂的机器学习任务。

       在图3.2中,错误分类的数据点用黑色填充,而正确分类的数据点没有填充。从填充的形状可以看出,感知器在学习可以将星星和圆分开的决策边界方面有困难。然而,MLP展现了一个很精确地对星和圆进行分类的决策边界。这是因为感知器没有像MLP那样的中间表示来分组和重新组织来处理数据的形状直至它们变成线性可分的,所以它不能将圆和星分开。

图3.2 感知器(左)和多层感知机(右)对XOR数据集的决策结果

3.2 在PyTorch中的实现

3.2.1 实例化MLP

       在实例化中,设置了两个线性模块fc1和fc2作为全连接层,其中第一个全连接层使用 ReLU 激活函数,作用于第一个线性层的输出并确保层中的输出数量等于下一层的输入数量;

  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. class MultilayerPerceptron(nn.Module):
  4. def __init__(self, input_dim, hidden_dim, output_dim):
  5. """
  6. 初始化多层感知器模型
  7. Args:
  8. input_dim (int): 输入向量的大小
  9. hidden_dim (int): 第一个全连接层的输出大小
  10. output_dim (int): 第二个全连接层的输出大小
  11. """
  12. super(MultilayerPerceptron, 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. Args:
  18. x_in (torch.Tensor): 输入数据张量。
  19. x_in.shape 应为 (batch, input_dim)
  20. apply_softmax (bool): 是否对输出进行 softmax 激活。
  21. 如果与交叉熵损失一起使用,则应为 False
  22. Returns:
  23. 结果张量。张量形状应为 (batch, output_dim)
  24. """
  25. intermediate = F.relu(self.fc1(x_in)) # 使用 ReLU 激活函数的第一个全连接层
  26. output = self.fc2(intermediate) # 第二个全连接层
  27. if apply_softmax:
  28. output = F.softmax(output, dim=1) # 如果应用 softmax,则在输出上进行 softmax 操作
  29. return output

输入维度大小为3,输出维度大小为4,和隐藏维度大小为100

  1. batch_size = 2 # 每次输入的样本数量
  2. input_dim = 3 # 输入向量的维度
  3. hidden_dim = 100 # 第一个全连接层的输出维度
  4. output_dim = 4 # 第二个全连接层的输出维度
  5. # 初始化模型
  6. mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
  7. print(mlp) # 打印模型结构

 MLP模型结构输出:

 3.2.2 模型完整性检查

  1. import torch
  2. def describe(x):
  3. """打印张量的类型、形状和数值"""
  4. print("Type: {}".format(x.type())) # 打印张量类型
  5. print("Shape/size: {}".format(x.shape)) # 打印张量形状
  6. print("Values: \n{}".format(x)) # 打印张量数值
  7. x_input = torch.rand(batch_size, input_dim) # 创建随机输入张量
  8. describe(x_input) # 调用 describe 函数打印张量信息
  9. y_output = mlp(x_input, apply_softmax=False) # 使用模型进行前向传播,不应用 softmax
  10. describe(y_output)

 x_input的输出结果:

y_output的输出结果:(softmax函数用于将一个值向量转换为概率)

 

        由上述输出分析可知mlp是将张量映射到其他张量的线性层,通过在每两层之间使用非线性来打破数据之间的线性关系,并允许模型扭曲向量空间以达到类之间的线性可分性。

3.3 姓氏分类任务(基于MLP)

3.3.1 数据集预处理

        姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,是作者从互联网上不同的姓名来源收集的。读取姓氏数据集并查看前五行示例:

  1. import collections
  2. import numpy as np
  3. import pandas as pd
  4. import re
  5. from argparse import Namespace
  6. args = Namespace(
  7. raw_dataset_csv="data/surnames/surnames.csv",
  8. train_proportion=0.7,
  9. val_proportion=0.15,
  10. test_proportion=0.15,
  11. output_munged_csv="data/surnames/surnames_with_splits.csv",
  12. seed=1337
  13. )
  14. # Read raw data
  15. surnames = pd.read_csv(args.raw_dataset_csv, header=0)
  16. surnames.head()

运行结果:

   数据集划分与保存:

  1. # 获取姓氏数据集中的所有国籍类别,并使用集合(set)确保每个国籍只出现一次
  2. set(surnames.nationality)
  3. # 按国籍划分训练集
  4. by_nationality = collections.defaultdict(list)
  5. for _, row in surnames.iterrows():
  6. by_nationality[row.nationality].append(row.to_dict())
  7. final_list = []
  8. np.random.seed(args.seed)
  9. for _, item_list in sorted(by_nationality.items()):
  10. np.random.shuffle(item_list)
  11. # 根据给定的比例计算每个数据集划分(训练集、验证集、测试集)的样本数量
  12. n = len(item_list)
  13. n_train = int(args.train_proportion*n)
  14. n_val = int(args.val_proportion*n)
  15. n_test = int(args.test_proportion*n)
  16. for item in item_list[:n_train]:
  17. item['split'] = 'train'
  18. for item in item_list[n_train:n_train+n_val]:
  19. item['split'] = 'val'
  20. for item in item_list[n_train+n_val:]:
  21. item['split'] = 'test'
  22. final_list.extend(item_list)
  23. # 从最终的数据点列表 final_list 中创建一个 DataFrame
  24. final_surnames = pd.DataFrame(final_list)
  25. final_surnames.split.value_counts()
  26. final_surnames.head()
  27. # 保存成csv文件
  28. final_surnames.to_csv(args.output_munged_csv, index=False)

 所有姓氏有:

每个数据集中的数据点数量:

DataFrame数据的前几行:

       该数据集内容是非常不平衡的,其中排名前三的课程占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。剩下的15个民族的频率也在下降——这也是语言特有的特性。另外,在国籍和姓氏正字法(拼写)之间有一种有效和直观的关系,有些拼写变体与原籍国联系非常紧密(比如“O ‘Neill”、“Antonopoulos”、“Nagasawa”或“Zhu”)。因此需要通过选择标记为俄语的姓氏的随机子集对这个过度代表的类进行子样本选择,来消除数据集的不平衡问题。另外,在数据集处理函数部分返回的是一个向量化的姓氏和与其国籍相对应的索引。

数据集处理类函数(SurnameDataset)如下:

  1. from torch.utils.data import Dataset, DataLoader
  2. class SurnameDataset(Dataset):
  3. def __init__(self, surname_df, vectorizer):
  4. """
  5. Args:
  6. surname_df (pandas.DataFrame): the dataset
  7. vectorizer (SurnameVectorizer): vectorizer instatiated from dataset
  8. """
  9. self.surname_df = surname_df
  10. self._vectorizer = vectorizer
  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. # Class weights
  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. """Load dataset and make a new vectorizer from scratch
  31. Args:
  32. surname_csv (str): location of the dataset
  33. Returns:
  34. an instance of 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. """Load dataset and the corresponding vectorizer.
  42. Used in the case in the vectorizer has been cached for re-use
  43. Args:
  44. surname_csv (str): location of the dataset
  45. vectorizer_filepath (str): location of the saved vectorizer
  46. Returns:
  47. an instance of SurnameDataset
  48. """
  49. surname_df = pd.read_csv(surname_csv)
  50. vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
  51. return cls(surname_df, vectorizer)
  52. @staticmethod
  53. def load_vectorizer_only(vectorizer_filepath):
  54. """a static method for loading the vectorizer from file
  55. Args:
  56. vectorizer_filepath (str): the location of the serialized vectorizer
  57. Returns:
  58. an instance of SurnameVectorizer
  59. """
  60. with open(vectorizer_filepath) as fp:
  61. return SurnameVectorizer.from_serializable(json.load(fp))
  62. def save_vectorizer(self, vectorizer_filepath):
  63. """saves the vectorizer to disk using json
  64. Args:
  65. vectorizer_filepath (str): the location to save the vectorizer
  66. """
  67. with open(vectorizer_filepath, "w") as fp:
  68. json.dump(self._vectorizer.to_serializable(), fp)
  69. def get_vectorizer(self):
  70. """ returns the vectorizer """
  71. return self._vectorizer
  72. def set_split(self, split="train"):
  73. """ selects the splits in the dataset using a column in the dataframe """
  74. self._target_split = split
  75. self._target_df, self._target_size = self._lookup_dict[split]
  76. def __len__(self):
  77. return self._target_size
  78. def __getitem__(self, index):
  79. """the primary entry point method for PyTorch datasets
  80. Args:
  81. index (int): the index to the data point
  82. Returns:
  83. a dictionary holding the data point's:
  84. features (x_surname)
  85. label (y_nationality)
  86. """
  87. row = self._target_df.iloc[index]
  88. surname_vector = \
  89. self._vectorizer.vectorize(row.surname)
  90. nationality_index = \
  91. self._vectorizer.nationality_vocab.lookup_token(row.nationality)
  92. return {'x_surname': surname_vector,
  93. 'y_nationality': nationality_index}
  94. def get_num_batches(self, batch_size):
  95. """Given a batch size, return the number of batches in the dataset
  96. Args:
  97. batch_size (int)
  98. Returns:
  99. number of batches in the dataset
  100. """
  101. return len(self) // batch_size
  102. def generate_batches(dataset, batch_size, shuffle=True,
  103. drop_last=True, device="cpu"):
  104. """
  105. A generator function which wraps the PyTorch DataLoader. It will
  106. ensure each tensor is on the write device location.
  107. """
  108. dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
  109. shuffle=shuffle, drop_last=drop_last)
  110. for data_dict in dataloader:
  111. out_data_dict = {}
  112. for name, tensor in data_dict.items():
  113. out_data_dict[name] = data_dict[name].to(device)
  114. yield out_data_dict

 需要用词汇表将姓氏字符串转换为向量化的minibatches以便机器读取并使用字符对姓氏进行分类的SurnameVectorizer()类代码如下:

  1. class SurnameVectorizer(object):
  2. """ The Vectorizer which coordinates the Vocabularies and puts them to use"""
  3. def __init__(self, surname_vocab, nationality_vocab):
  4. """
  5. Args:
  6. surname_vocab (Vocabulary): maps characters to integers
  7. nationality_vocab (Vocabulary): maps nationalities to integers
  8. """
  9. self.surname_vocab = surname_vocab
  10. self.nationality_vocab = nationality_vocab
  11. def vectorize(self, surname):
  12. """
  13. Args:
  14. surname (str): the surname
  15. Returns:
  16. one_hot (np.ndarray): a collapsed one-hot encoding
  17. """
  18. vocab = self.surname_vocab
  19. one_hot = np.zeros(len(vocab), dtype=np.float32)
  20. for token in surname:
  21. one_hot[vocab.lookup_token(token)] = 1
  22. return one_hot
  23. @classmethod
  24. def from_dataframe(cls, surname_df):
  25. """Instantiate the vectorizer from the dataset dataframe
  26. Args:
  27. surname_df (pandas.DataFrame): the surnames dataset
  28. Returns:
  29. an instance of the SurnameVectorizer
  30. """
  31. surname_vocab = Vocabulary(unk_token="@")
  32. nationality_vocab = Vocabulary(add_unk=False)
  33. for index, row in surname_df.iterrows():
  34. for letter in row.surname:
  35. surname_vocab.add_token(letter)
  36. nationality_vocab.add_token(row.nationality)
  37. return cls(surname_vocab, nationality_vocab)
  38. @classmethod
  39. def from_serializable(cls, contents):
  40. surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
  41. nationality_vocab = Vocabulary.from_serializable(contents['nationality_vocab'])
  42. return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab)
  43. def to_serializable(self):
  44. return {'surname_vocab': self.surname_vocab.to_serializable(),
  45. 'nationality_vocab': self.nationality_vocab.to_serializable()}

用于处理文本和提取映射词汇的类Vocabulary()完整代码:

  1. class Vocabulary(object):
  2. """Class to process text and extract vocabulary for mapping"""
  3. def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
  4. """
  5. Args:
  6. token_to_idx (dict): a pre-existing map of tokens to indices
  7. add_unk (bool): a flag that indicates whether to add the UNK token
  8. unk_token (str): the UNK token to add into the Vocabulary
  9. """
  10. if token_to_idx is None:
  11. token_to_idx = {}
  12. self._token_to_idx = token_to_idx
  13. self._idx_to_token = {idx: token
  14. for token, idx in self._token_to_idx.items()}
  15. self._add_unk = add_unk
  16. self._unk_token = unk_token
  17. self.unk_index = -1
  18. if add_unk:
  19. self.unk_index = self.add_token(unk_token)
  20. def to_serializable(self):
  21. """ returns a dictionary that can be serialized """
  22. return {'token_to_idx': self._token_to_idx,
  23. 'add_unk': self._add_unk,
  24. 'unk_token': self._unk_token}
  25. @classmethod
  26. def from_serializable(cls, contents):
  27. """ instantiates the Vocabulary from a serialized dictionary """
  28. return cls(**contents)
  29. def add_token(self, token):
  30. """Update mapping dicts based on the token.
  31. Args:
  32. token (str): the item to add into the Vocabulary
  33. Returns:
  34. index (int): the integer corresponding to the token
  35. """
  36. try:
  37. index = self._token_to_idx[token]
  38. except KeyError:
  39. index = len(self._token_to_idx)
  40. self._token_to_idx[token] = index
  41. self._idx_to_token[index] = token
  42. return index
  43. def add_many(self, tokens):
  44. """Add a list of tokens into the Vocabulary
  45. Args:
  46. tokens (list): a list of string tokens
  47. Returns:
  48. indices (list): a list of indices corresponding to the tokens
  49. """
  50. return [self.add_token(token) for token in tokens]
  51. def lookup_token(self, token):
  52. """Retrieve the index associated with the token
  53. or the UNK index if token isn't present.
  54. Args:
  55. token (str): the token to look up
  56. Returns:
  57. index (int): the index corresponding to the token
  58. Notes:
  59. `unk_index` needs to be >=0 (having been added into the Vocabulary)
  60. for the UNK functionality
  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. """Return the token associated with the index
  68. Args:
  69. index (int): the index to look up
  70. Returns:
  71. token (str): the token corresponding to the index
  72. Raises:
  73. KeyError: if the index is not in the Vocabulary
  74. """
  75. if index not in self._idx_to_token:
  76. raise KeyError("the index (%d) is not in the Vocabulary" % index)
  77. return self._idx_to_token[index]
  78. def __str__(self):
  79. return "<Vocabulary(size=%d)>" % len(self)
  80. def __len__(self):
  81. return len(self._token_to_idx)

 同上述3.2.1中的实例化MLP,最后的softmax是否应用取决于是否需要确保输出和为1,完整的MLP实例化代码如下:

  1. class SurnameClassifier(nn.Module):
  2. """ A 2-layer Multilayer Perceptron for classifying surnames """
  3. def __init__(self, input_dim, hidden_dim, output_dim):
  4. """
  5. Args:
  6. input_dim (int): the size of the input vectors
  7. hidden_dim (int): the output size of the first Linear layer
  8. output_dim (int): the output size of the second Linear layer
  9. """
  10. super(SurnameClassifier, self).__init__()
  11. self.fc1 = nn.Linear(input_dim, hidden_dim)
  12. self.fc2 = nn.Linear(hidden_dim, output_dim)
  13. def forward(self, x_in, apply_softmax=False):
  14. """The forward pass of the classifier
  15. Args:
  16. x_in (torch.Tensor): an input data tensor.
  17. x_in.shape should be (batch, input_dim)
  18. apply_softmax (bool): a flag for the softmax activation
  19. should be false if used with the Cross Entropy losses
  20. Returns:
  21. the resulting tensor. tensor.shape should be (batch, output_dim)
  22. """
  23. intermediate_vector = F.relu(self.fc1(x_in))
  24. prediction_vector = self.fc2(intermediate_vector)
  25. if apply_softmax:
  26. prediction_vector = F.softmax(prediction_vector, dim=1)
  27. return prediction_vector

3.3.2  Training Routine(训练例程)

基于本实验,MLP进行姓氏分类所需的参数结构如下:

  1. args = Namespace(
  2. # Data and path information
  3. surname_csv="data/surnames/surnames_with_splits.csv",
  4. vectorizer_file="vectorizer.json",
  5. model_state_file="model.pth",
  6. save_dir="model_storage/ch4/surname_mlp",
  7. # Model hyper parameters
  8. hidden_dim=300,
  9. # Training hyper parameters
  10. seed=1337,
  11. num_epochs=100,
  12. early_stopping_criteria=5,
  13. learning_rate=0.001,
  14. batch_size=64,
  15. # Runtime options
  16. cuda=False,
  17. reload_from_files=False,
  18. expand_filepaths_to_save_dir=True,
  19. )
  20. if args.expand_filepaths_to_save_dir:
  21. args.vectorizer_file = os.path.join(args.save_dir,
  22. args.vectorizer_file)
  23. args.model_state_file = os.path.join(args.save_dir,
  24. args.model_state_file)
  25. print("Expanded filepaths: ")
  26. print("\t{}".format(args.vectorizer_file))
  27. print("\t{}".format(args.model_state_file))
  28. # Check 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("Using CUDA: {}".format(args.cuda))
  33. # Set seed for reproducibility
  34. set_seed_everywhere(args.seed, args.cuda)
  35. # handle dirs
  36. handle_dirs(args.save_dir)

输出结果:

其中要用到一些函数:

  1. def make_train_state(args):
  2. return {'stop_early': False,
  3. 'early_stopping_step': 0,
  4. 'early_stopping_best_val': 1e8,
  5. 'learning_rate': args.learning_rate,
  6. 'epoch_index': 0,
  7. 'train_loss': [],
  8. 'train_acc': [],
  9. 'val_loss': [],
  10. 'val_acc': [],
  11. 'test_loss': -1,
  12. 'test_acc': -1,
  13. 'model_filename': args.model_state_file}
  14. def update_train_state(args, model, train_state):
  15. """Handle the training state updates.
  16. Components:
  17. - Early Stopping: Prevent overfitting.
  18. - Model Checkpoint: Model is saved if the model is better
  19. :param args: main arguments
  20. :param model: model to train
  21. :param train_state: a dictionary representing the training state values
  22. :returns:
  23. a new train_state
  24. """
  25. # Save one model at least
  26. if train_state['epoch_index'] == 0:
  27. torch.save(model.state_dict(), train_state['model_filename'])
  28. train_state['stop_early'] = False
  29. # Save model if performance improved
  30. elif train_state['epoch_index'] >= 1:
  31. loss_tm1, loss_t = train_state['val_loss'][-2:]
  32. # If loss worsened
  33. if loss_t >= train_state['early_stopping_best_val']:
  34. # Update step
  35. train_state['early_stopping_step'] += 1
  36. # Loss decreased
  37. else:
  38. # Save the best model
  39. if loss_t < train_state['early_stopping_best_val']:
  40. torch.save(model.state_dict(), train_state['model_filename'])
  41. # Reset early stopping step
  42. train_state['early_stopping_step'] = 0
  43. # Stop early ?
  44. train_state['stop_early'] = \
  45. train_state['early_stopping_step'] >= args.early_stopping_criteria
  46. return train_state
  47. def compute_accuracy(y_pred, y_target):
  48. _, y_pred_indices = y_pred.max(dim=1)
  49. n_correct = torch.eq(y_pred_indices, y_target).sum().item()
  50. return n_correct / len(y_pred_indices) * 100
  51. def set_seed_everywhere(seed, cuda):
  52. np.random.seed(seed)
  53. torch.manual_seed(seed)
  54. if cuda:
  55. torch.cuda.manual_seed_all(seed)
  56. def handle_dirs(dirpath):
  57. if not os.path.exists(dirpath):
  58. os.makedirs(dirpath)

实例化数据集、模型、损失和优化器:

  1. import pandas as pd
  2. # 加载数据集并创建Vectorizer
  3. dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
  4. vectorizer = dataset.get_vectorizer()
  5. # 初始化分类器
  6. classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab),
  7. hidden_dim=args.hidden_dim,
  8. output_dim=len(vectorizer.nationality_vocab))
  9. # 将分类器移到指定的设备上
  10. classifier = classifier.to(args.device)
  11. # 定义损失函数为交叉熵损失,并考虑类别权重
  12. loss_func = nn.CrossEntropyLoss(dataset.class_weights)
  13. # 定义优化器为Adam,并传入分类器的参数和学习率
  14. optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)

       使用不同的key从batch_dict中获取数据,利用训练数据,计算模型输出、损失和梯度。然后,使用梯度来更新模型,循环代码如下:

  1. from tqdm import tqdm_notebook
  2. # 创建用于显示进度条的epoch_bar,用于跟踪训练过程中的epoch
  3. epoch_bar = tqdm_notebook(desc='training routine',
  4. total=args.num_epochs, # 总共的epoch数量
  5. position=0) # 进度条在显示中的位置
  6. # 设置数据集的split为训练集
  7. dataset.set_split('train')
  8. # 创建用于显示训练集进度的train_bar,用于跟踪每个epoch中的批次
  9. train_bar = tqdm_notebook(desc='split=train', # 进度条的描述
  10. total=dataset.get_num_batches(args.batch_size), # 总共的批次数量
  11. position=1, # 进度条在显示中的位置
  12. leave=True) # 训练完成后是否保留进度条
  13. # 设置数据集的split为验证集
  14. dataset.set_split('val')
  15. # 创建用于显示验证集进度的val_bar,用于跟踪每个epoch中的批次
  16. val_bar = tqdm_notebook(desc='split=val', # 进度条的描述
  17. total=dataset.get_num_batches(args.batch_size), # 总共的批次数量
  18. position=1, # 进度条在显示中的位置
  19. leave=True) # 训练完成后是否保留进度条
  20. try:
  21. for epoch_index in range(args.num_epochs):
  22. train_state['epoch_index'] = epoch_index
  23. # Iterate over training dataset
  24. # setup: batch generator, set loss and acc to 0, set train mode on
  25. dataset.set_split('train')
  26. batch_generator = generate_batches(dataset,
  27. batch_size=args.batch_size,
  28. device=args.device)
  29. running_loss = 0.0
  30. running_acc = 0.0
  31. classifier.train()
  32. for batch_index, batch_dict in enumerate(batch_generator):
  33. # the training routine is these 5 steps:
  34. # --------------------------------------
  35. # step 1. zero the gradients
  36. optimizer.zero_grad()
  37. # step 2. compute the output
  38. y_pred = classifier(batch_dict['x_surname'])
  39. # step 3. compute the loss
  40. loss = loss_func(y_pred, batch_dict['y_nationality'])
  41. loss_t = loss.item()
  42. running_loss += (loss_t - running_loss) / (batch_index + 1)
  43. # step 4. use loss to produce gradients
  44. loss.backward()
  45. # step 5. use optimizer to take gradient step
  46. optimizer.step()
  47. # -----------------------------------------
  48. # compute the accuracy
  49. acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
  50. running_acc += (acc_t - running_acc) / (batch_index + 1)
  51. # update bar
  52. train_bar.set_postfix(loss=running_loss, acc=running_acc,
  53. epoch=epoch_index)
  54. train_bar.update()
  55. train_state['train_loss'].append(running_loss)
  56. train_state['train_acc'].append(running_acc)
  57. # Iterate over val dataset
  58. # setup: batch generator, set loss and acc to 0; set eval mode on
  59. dataset.set_split('val')
  60. batch_generator = generate_batches(dataset,
  61. batch_size=args.batch_size,
  62. device=args.device)
  63. running_loss = 0.
  64. running_acc = 0.
  65. classifier.eval()
  66. for batch_index, batch_dict in enumerate(batch_generator):
  67. # compute the output
  68. y_pred = classifier(batch_dict['x_surname'])
  69. # step 3. compute the loss
  70. loss = loss_func(y_pred, batch_dict['y_nationality'])
  71. loss_t = loss.item()
  72. running_loss += (loss_t - running_loss) / (batch_index + 1)
  73. # compute the accuracy
  74. acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
  75. running_acc += (acc_t - running_acc) / (batch_index + 1)
  76. val_bar.set_postfix(loss=running_loss, acc=running_acc,
  77. epoch=epoch_index)
  78. val_bar.update()
  79. train_state['val_loss'].append(running_loss)
  80. train_state['val_acc'].append(running_acc)
  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")

循环的结果:

3.3.3 模型测试

  1. # compute the loss & accuracy on the test set using the best available model
  2. classifier.load_state_dict(torch.load(train_state['model_filename']))
  3. classifier = classifier.to(args.device)
  4. dataset.class_weights = dataset.class_weights.to(args.device)
  5. loss_func = nn.CrossEntropyLoss(dataset.class_weights)
  6. dataset.set_split('test')
  7. batch_generator = generate_batches(dataset,
  8. batch_size=args.batch_size,
  9. device=args.device)
  10. running_loss = 0.
  11. running_acc = 0.
  12. classifier.eval()
  13. for batch_index, batch_dict in enumerate(batch_generator):
  14. # compute the output
  15. y_pred = classifier(batch_dict['x_surname'])
  16. # compute the loss
  17. loss = loss_func(y_pred, batch_dict['y_nationality'])
  18. loss_t = loss.item()
  19. running_loss += (loss_t - running_loss) / (batch_index + 1)
  20. # compute the accuracy
  21. acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
  22. running_acc += (acc_t - running_acc) / (batch_index + 1)
  23. train_state['test_loss'] = running_loss
  24. train_state['test_acc'] = running_acc
  25. print("Test loss: {};".format(train_state['test_loss']))
  26. print("Test Accuracy: {}".format(train_state['test_acc']))

测试结果与准确率:

模型对测试数据的准确性达到50%左右,在训练数据上的准确率会更高,但是总体的准确率都不高,这是因为one-hot向量表示丢弃了字符之间的顺序信息,是一种弱表示; 

3.3.4 模型评估和预测

还需要测试该模型对新数据的预测结果来判断模型的好坏,给定一个姓氏作为字符串,首先进行向量化过程,然后进行模型预测;

  1. def predict_nationality(surname, classifier, vectorizer):
  2. """Predict the nationality from a new surname
  3. Args:
  4. surname (str): the surname to classifier
  5. classifier (SurnameClassifer): an instance of the classifier
  6. vectorizer (SurnameVectorizer): the corresponding vectorizer
  7. Returns:
  8. a dictionary with the most likely nationality and its probability
  9. """
  10. vectorized_surname = vectorizer.vectorize(surname)
  11. vectorized_surname = torch.tensor(vectorized_surname).view(1, -1)
  12. result = classifier(vectorized_surname, apply_softmax=True)
  13. probability_values, indices = result.max(dim=1)
  14. index = indices.item()
  15. predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
  16. probability_value = probability_values.item()
  17. return {'nationality': predicted_nationality, 'probability': probability_value}
  18. new_surname = input("Enter a surname to classify: ")
  19. classifier = classifier.to("cpu")
  20. prediction = predict_nationality(new_surname, classifier, vectorizer)
  21. print("{} -> {} (p={:0.2f})".format(new_surname,
  22. prediction['nationality'],
  23. prediction['probability']))

将apply_softmax标志设置为True,结果将为类别概率预测,使用PyTorch张量最大函数得到由最高预测概率表示的最优类,即作为预测结果:

 对代码做一些修改,通过torch.topk函数使用K-best方法可以检索模型预测概率最大的前K个结果,以便进行对比:

  1. vectorizer.nationality_vocab.lookup_index(8)
  2. def predict_topk_nationality(name, classifier, vectorizer, k=5):
  3. vectorized_name = vectorizer.vectorize(name)
  4. vectorized_name = torch.tensor(vectorized_name).view(1, -1)
  5. prediction_vector = classifier(vectorized_name, apply_softmax=True)
  6. probability_values, indices = torch.topk(prediction_vector, k=k)
  7. # returned size is 1,k
  8. probability_values = probability_values.detach().numpy()[0]
  9. indices = indices.detach().numpy()[0]
  10. results = []
  11. for prob_value, index in zip(probability_values, indices):
  12. nationality = vectorizer.nationality_vocab.lookup_index(index)
  13. results.append({'nationality': nationality,
  14. 'probability': prob_value})
  15. return results
  16. new_surname = input("Enter a surname to classify: ")
  17. classifier = classifier.to("cpu")
  18. k = int(input("How many of the top predictions to see? "))
  19. if k > len(vectorizer.nationality_vocab):
  20. print("Sorry! That's more than the # of nationalities we have.. defaulting you to max size :)")
  21. k = len(vectorizer.nationality_vocab)
  22. predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)
  23. print("Top {} predictions:".format(k))
  24. print("===================")
  25. for prediction in predictions:
  26. print("{} -> {} (p={:0.2f})".format(new_surname,
  27. prediction['nationality'],
  28. prediction['probability']))

输出结果如下:

 

3.3.5 Dropout解决过拟合(结构正则化)

在模型训练过程中经常出现过拟合的现象,解决过拟合问题有两种重要的权重正则化类型——L1和L2正则化,除此之外,对于MLP这种深度模型来说,结构正则化方法也非常常用,即Dropout,添加Dropout的实例化MLP模型如下:

      # 在第一个全连接层的输出上应用dropout,然后传递给第二个全连接层
        output = self.fc2(F.dropout(intermediate, p=0.5))

  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. class MultilayerPerceptron(nn.Module):
  4. def __init__(self, input_dim, hidden_dim, output_dim):
  5. """
  6. 初始化多层感知机模型
  7. Args:
  8. input_dim (int): 输入向量的大小
  9. hidden_dim (int): 第一个线性层的输出大小
  10. output_dim (int): 第二个线性层的输出大小
  11. """
  12. super(MultilayerPerceptron, self).__init__()
  13. # 定义第一个全连接层
  14. self.fc1 = nn.Linear(input_dim, hidden_dim)
  15. # 定义第二个全连接层
  16. self.fc2 = nn.Linear(hidden_dim, output_dim)
  17. def forward(self, x_in, apply_softmax=False):
  18. """
  19. MLP的前向传播
  20. Args:
  21. x_in (torch.Tensor): 输入数据张量,x_in.shape应为(batch, input_dim)
  22. apply_softmax (bool): 是否应用softmax激活函数,如果用于交叉熵损失,应设置为False
  23. Returns:
  24. 结果张量,tensor.shape应为(batch, output_dim)
  25. """
  26. # 使用ReLU激活函数的第一个全连接层
  27. intermediate = F.relu(self.fc1(x_in))
  28. # 在第一个全连接层的输出上应用dropout,然后传递给第二个全连接层
  29. output = self.fc2(F.dropout(intermediate, p=0.5))
  30. # 如果apply_softmax为True,则应用softmax激活函数
  31. if apply_softmax:
  32. output = F.softmax(output, dim=1)
  33. return output

由测试结果可知在加入过拟合处理后,模型准确率会有一定提高,但不会大幅度提升(结果截图没有保存)。

四、卷积神经网络(CNN)

4.1 CNN介绍

       这是一种非常适合检测空间子结构(并因此创建有意义的空间子结构)的神经网络。CNNs通过使用少量的权重来扫描输入数据张量来实现这一点。通过这种扫描,它们产生表示子结构检测(或不检测)的输出张量。

       CNN的结构可以从四个方面进行分析,一是输入层:本实验的输入即为预处理后的姓氏数据集信息;

二是卷积层(Convolutional Layer):卷积层是CNN的核心组成部分,由多个滤波器(或卷积核)组成,每个滤波器与输入数据进行卷积操作。滤波器通过滑动窗口的方式在输入数据上移动,计算每个位置的卷积结果,从而提取局部特征。每个滤波器在不同位置的计算共享参数,从而减少模型的参数数量和计算复杂度。其中,每个卷积层后通常会添加非线性激活函数,激活函数类型与MLP模型相同,如ReLU等,用于引入非线性特征并增加网络的表达能力;

三是池化层(Pooling Layer):池化层用于减少卷积层输出的空间维度,包括降低数据体积、参数数量,以及控制过拟合。常用的池化操作包括最大池化(Max Pooling)和平均池化(Average Pooling),它们分别取池化窗口中的最大值或平均值作为输出;

四是全连接层(Fully Connected Layer):在CNN的顶部,通常会添加全连接层,用于将卷积层和池化层提取的特征映射转换为最终的输出。全连接层的每个神经元与前一层的所有神经元相连,执行分类或回归等任务;

五是输出层:根据全连接层的信息得到概率最大的结果即为输出。

       具体的卷积运算如图4.1,输入矩阵(4X4的矩阵)与单个产生输出矩阵的卷积核(3X3矩阵)每次移动卷积核都将自身的值乘以输入矩阵对应位置的值,然后将这些乘法相加作为映射位置的输出值(2X2矩阵),就是一个降维的过程。另外,CNN的设计中超参数的设定尤为重要,一般包括卷积核大小(Kernel_size)、输入张量填充(Padding)、步长(stride)、通道数(channel)等,需要根据实验需求灵活选取,且取值的合理性会很大的影响模型效果。

图 4.1 卷积示例

4.2 在pytorch中实现CNN

       4.2.1 减小张量的方法

        第一步是将PyTorch的Conv1d类的一个实例应用到三维数据张量,通过检查输出的大小,可以知道张量减少了多少:

  1. batch_size = 2 # 批量大小
  2. one_hot_size = 10 # one-hot编码的大小
  3. sequence_width = 7 # 序列宽度
  4. # 生成随机数据,形状为(batch_size, one_hot_size, sequence_width)
  5. data = torch.randn(batch_size, one_hot_size, sequence_width)
  6. # 创建一个一维卷积层,输入通道数为one_hot_size,输出通道数为16,卷积核大小为3
  7. conv1 = Conv1d(in_channels=one_hot_size, out_channels=16,
  8. kernel_size=3)
  9. # 将数据传递给卷积层进行前向传播
  10. intermediate1 = conv1(data)
  11. # 打印原始数据和经过第一个卷积层后的形状
  12. print(data.size()) # 打印原始数据形状
  13. print(intermediate1.size()) # 打印经过第一个卷积层后的形状

 运行结果可以看出数据形状的减小:

通过添加额外的卷积可以减小张量,在应用两个额外卷积之后,输出结果在最终维度上的大小才为1:

  1. # 设置卷积层
  2. conv2 = nn.Conv1d(in_channels=16, out_channels=32, kernel_size=3)
  3. conv3 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3)
  4. intermediate2 = conv2(intermediate1)
  5. intermediate3 = conv3(intermediate2)
  6. #打印卷积后的结果
  7. print(intermediate2.size())
  8. print(intermediate3.size())

在每次卷积中,通道维数的大小都会增加,因为通道维数是每个数据点的特征向量: 

 

  1. y_output = intermediate3.squeeze() #squeeze()从张量中移除大小为1的维度
  2. print(y_output.size())#打印张量 y_output 的大小

 使用squeeze()方法去掉不需要的尺寸(1维),输出结果如下:

 

另外,减小张量还可以使用PyTorch的view()方法将所有向量平展成单个向量(即Method2),或者通过求算术平均值/最大值/沿feature map维数求和的方法(即Method3):

  1. # Method 2 of reducing to feature vectors
  2. print(intermediate1.view(batch_size, -1).size())
  3. # Method 3 of reducing to feature vectors
  4. print(torch.mean(intermediate1, dim=2).size())
  5. # print(torch.max(intermediate1, dim=2).size())
  6. # print(torch.sum(intermediate1, dim=2).size())

示例结果如下:

 

4.2.2 数据处理函数

 CNN实现姓氏分类的主要函数类如下,主要包含三个类,该部分与MLP基本相同,不做赘述。

Vocabulary类
  1. # 导入必要的库和模块
  2. from argparse import Namespace
  3. from collections import Counter
  4. import json
  5. import os
  6. import string
  7. import numpy as np
  8. import pandas as pd
  9. import torch
  10. import torch.nn as nn
  11. import torch.nn.functional as F
  12. import torch.optim as optim
  13. from torch.utils.data import Dataset, DataLoader
  14. from tqdm import tqdm_notebook
  15. class Vocabulary:
  16. """用于处理文本并提取词汇表的类"""
  17. def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
  18. """
  19. 初始化Vocabulary类
  20. Args:
  21. token_to_idx (dict): 一个现有的将标记映射到索引的字典
  22. add_unk (bool): 一个指示是否添加UNK标记的标志
  23. unk_token (str): 要添加到词汇表中的UNK标记
  24. """
  25. # 如果token_to_idx为None,则初始化为空字典
  26. if token_to_idx is None:
  27. token_to_idx = {}
  28. self._token_to_idx = token_to_idx
  29. # 创建从索引到标记的映射字典
  30. self._idx_to_token = {idx: token
  31. for token, idx in self._token_to_idx.items()}
  32. # 设置是否添加UNK标记和UNK标记的值
  33. self._add_unk = add_unk
  34. self._unk_token = unk_token
  35. # 如果需要添加UNK标记,则添加UNK标记并获取其索引
  36. self.unk_index = -1
  37. if add_unk:
  38. self.unk_index = self.add_token(unk_token)
  39. def to_serializable(self):
  40. """返回一个可序列化的字典"""
  41. return {'token_to_idx': self._token_to_idx,
  42. 'add_unk': self._add_unk,
  43. 'unk_token': self._unk_token}
  44. @classmethod
  45. def from_serializable(cls, contents):
  46. """从一个序列化的字典实例化Vocabulary"""
  47. return cls(**contents)
  48. def add_token(self, token):
  49. """根据标记更新映射字典。
  50. Args:
  51. token (str): 要添加到词汇表中的项
  52. Returns:
  53. index (int): 与标记对应的整数
  54. """
  55. try:
  56. index = self._token_to_idx[token]
  57. except KeyError:
  58. # 如果标记不在词汇表中,则将其添加并获取新的索引
  59. index = len(self._token_to_idx)
  60. self._token_to_idx[token] = index
  61. self._idx_to_token[index] = token
  62. return index
  63. def add_many(self, tokens):
  64. """Add a list of tokens into the Vocabulary
  65. Args:
  66. tokens (list): a list of string tokens
  67. Returns:
  68. indices (list): a list of indices corresponding to the tokens
  69. """
  70. # 使用列表推导将每个标记添加到词汇表中,并获取对应的索引
  71. return [self.add_token(token) for token in tokens]
  72. def lookup_token(self, token):
  73. """Retrieve the index associated with the token
  74. or the UNK index if token isn't present.
  75. Args:
  76. token (str): the token to look up
  77. Returns:
  78. index (int): the index corresponding to the token
  79. Notes:
  80. `unk_index` needs to be >=0 (having been added into the Vocabulary)
  81. for the UNK functionality
  82. """
  83. # 如果存在UNK索引,则使用get方法检索标记的索引,否则直接检索
  84. if self.unk_index >= 0:
  85. return self._token_to_idx.get(token, self.unk_index)
  86. else:
  87. return self._token_to_idx[token]
  88. def lookup_index(self, index):
  89. """返回与索引关联的标记
  90. Args:
  91. index (int): 要查找的索引
  92. Returns:
  93. token (str): 与索引对应的标记
  94. Raises:
  95. KeyError: 如果索引不在词汇表中
  96. """
  97. # 检查索引是否在索引到标记的映射字典中,如果不存在,则引发KeyError
  98. if index not in self._idx_to_token:
  99. raise KeyError("the index (%d) is not in the Vocabulary" % index)
  100. # 返回与索引对应的标记
  101. return self._idx_to_token[index]
  102. def __str__(self):
  103. return "<Vocabulary(size=%d)>" % len(self)
  104. def __len__(self):
  105. return len(self._token_to_idx)
 SurnameVectorizer类

        尽管词汇表和DataLoader的实现方式与MLP中的相同,但Vectorizer的vectorize()方法已经更改,以适应CNN模型的需要。具体来说,该函数将字符串中的每个字符映射到一个整数,然后使用该整数构造一个由onehot向量组成的矩阵,矩阵中的每一列都是不同的onehot向量。主要原因是,我们将使用的Conv1d层要求数据张量在第0维上具有批处理,在第1维上具有通道,在第2维上具有特性,还修改了矢量化器,以便计算姓氏的最大长度并将其保存为max_surname_length。

  1. class SurnameVectorizer(object):
  2. """
  3. SurnameVectorizer类用于处理姓氏数据的向量化,包括构建姓氏和国籍的词汇表并将其应用于数据向量化。
  4. Attributes:
  5. surname_vocab (Vocabulary): 姓氏词汇表,用于将姓氏转换为向量表示。
  6. nationality_vocab (Vocabulary): 国籍词汇表,用于将国籍转换为向量表示。
  7. """
  8. def __init__(self, surname_vocab, nationality_vocab):
  9. """
  10. 初始化SurnameVectorizer对象。
  11. Args:
  12. surname_vocab (Vocabulary): 姓氏词汇表。
  13. nationality_vocab (Vocabulary): 国籍词汇表。
  14. """
  15. self.surname_vocab = surname_vocab
  16. self.nationality_vocab = nationality_vocab
  17. def vectorize(self, surname):
  18. """
  19. 将姓氏向量化。
  20. Args:
  21. surname (str): 姓氏字符串。
  22. Returns:
  23. np.ndarray: 姓氏的向量表示(折叠的独热编码)。
  24. """
  25. vocab = self.surname_vocab
  26. one_hot = np.zeros(len(vocab), dtype=np.float32)
  27. for token in surname:
  28. one_hot[vocab.lookup_token(token)] = 1
  29. return one_hot
  30. @classmethod
  31. def from_dataframe(cls, surname_df):
  32. """
  33. 从数据集DataFrame实例化Vectorizer对象。
  34. Args:
  35. surname_df (pandas.DataFrame): 姓氏数据集DataFrame。
  36. Returns:
  37. SurnameVectorizer: 实例化的SurnameVectorizer对象。
  38. """
  39. surname_vocab = Vocabulary(unk_token="@")
  40. nationality_vocab = Vocabulary(add_unk=False)
  41. for index, row in surname_df.iterrows():
  42. for letter in row.surname:
  43. surname_vocab.add_token(letter)
  44. nationality_vocab.add_token(row.nationality)
  45. return cls(surname_vocab, nationality_vocab)
  46. @classmethod
  47. def from_serializable(cls, contents):
  48. """
  49. 从可序列化内容实例化Vectorizer对象。
  50. Args:
  51. contents (dict): 包含可序列化内容的字典。
  52. Returns:
  53. SurnameVectorizer: 实例化的SurnameVectorizer对象。
  54. """
  55. surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
  56. nationality_vocab = Vocabulary.from_serializable(contents['nationality_vocab'])
  57. return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab)
  58. def to_serializable(self):
  59. """
  60. 将Vectorizer对象转换为可序列化内容。
  61. Returns:
  62. dict: 包含可序列化内容的字典。
  63. """
  64. return {'surname_vocab': self.surname_vocab.to_serializable(),
  65. 'nationality_vocab': self.nationality_vocab.to_serializable()}
 SurnameDataset类:

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

  1. class SurnameDataset(Dataset):
  2. def __init__(self, surname_df, vectorizer):
  3. """
  4. 初始化SurnameDataset类
  5. Args:
  6. surname_df (pandas.DataFrame): 数据集
  7. vectorizer (SurnameVectorizer): 从数据集实例化的矢量化器
  8. """
  9. # 保存数据集和矢量化器
  10. self.surname_df = surname_df
  11. self._vectorizer = vectorizer
  12. # 根据数据集的拆分设置训练集、验证集和测试集
  13. self.train_df = self.surname_df[self.surname_df.split=='train']
  14. self.train_size = len(self.train_df)
  15. self.val_df = self.surname_df[self.surname_df.split=='val']
  16. self.validation_size = len(self.val_df)
  17. self.test_df = self.surname_df[self.surname_df.split=='test']
  18. self.test_size = len(self.test_df)
  19. # 创建一个字典来存储各个拆分的数据集和大小
  20. self._lookup_dict = {'train': (self.train_df, self.train_size),
  21. 'val': (self.val_df, self.validation_size),
  22. 'test': (self.test_df, self.test_size)}
  23. # 设置当前使用的数据集拆分,默认为训练集
  24. self.set_split('train')
  25. # 类别权重
  26. # 统计各个国籍的样本数量
  27. class_counts = surname_df.nationality.value_counts().to_dict()
  28. # 定义排序关键字函数
  29. def sort_key(item):
  30. return self._vectorizer.nationality_vocab.lookup_token(item[0])
  31. # 按国籍词汇表的顺序对样本数量进行排序
  32. sorted_counts = sorted(class_counts.items(), key=sort_key)
  33. # 获取排序后的样本频率
  34. frequencies = [count for _, count in sorted_counts]
  35. # 计算类别权重,即样本频率的倒数
  36. self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)
  37. @classmethod
  38. def load_dataset_and_make_vectorizer(cls, surname_csv):
  39. """加载数据集并从头创建一个新的矢量化器
  40. Args:
  41. surname_csv (str): 数据集的位置
  42. Returns:
  43. SurnameDataset的实例
  44. """
  45. # 从CSV文件中读取姓氏数据集
  46. surname_df = pd.read_csv(surname_csv)
  47. # 从数据集中获取训练集的部分
  48. train_surname_df = surname_df[surname_df.split=='train']
  49. # 使用训练集的部分创建一个新的矢量化器
  50. return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
  51. @classmethod
  52. def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
  53. """加载数据集和相应的矢量化器。
  54. 在矢量化器已被缓存以便重复使用的情况下使用。
  55. Args:
  56. surname_csv (str): 数据集的位置
  57. vectorizer_filepath (str): 保存的矢量化器的位置
  58. Returns:
  59. SurnameDataset的实例
  60. """
  61. # 从CSV文件中读取姓氏数据集
  62. surname_df = pd.read_csv(surname_csv)
  63. # 加载已保存的矢量化器
  64. vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
  65. # 返回一个数据集实例,同时传入数据集和加载的矢量化器
  66. return cls(surname_df, vectorizer)
  67. @staticmethod
  68. def load_vectorizer_only(vectorizer_filepath):
  69. """从文件加载矢量化器的静态方法
  70. Args:
  71. vectorizer_filepath (str): 序列化矢量化器的位置
  72. Returns:
  73. SurnameVectorizer的实例
  74. """
  75. # 使用json.load从文件中加载序列化的矢量化器,并返回一个实例
  76. with open(vectorizer_filepath) as fp:
  77. return SurnameVectorizer.from_serializable(json.load(fp))
  78. def save_vectorizer(self, vectorizer_filepath):
  79. """使用json将矢量化器保存到磁盘
  80. Args:
  81. vectorizer_filepath (str): 要保存矢量化器的位置
  82. """
  83. # 使用json.dump将矢量化器序列化并保存到文件中
  84. with open(vectorizer_filepath, "w") as fp:
  85. json.dump(self._vectorizer.to_serializable(), fp)
  86. def get_vectorizer(self):
  87. """返回矢量化器"""
  88. return self._vectorizer
  89. def set_split(self, split="train"):
  90. """根据DataFrame中的列选择数据集的拆分"""
  91. # 设置目标拆分和对应的DataFrame以及大小
  92. self._target_split = split
  93. self._target_df, self._target_size = self._lookup_dict[split]
  94. def __len__(self):
  95. """返回数据集的大小"""
  96. return self._target_size
  97. def __getitem__(self, index):
  98. """PyTorch数据集的主要入口方法
  99. Args:
  100. index (int): 数据点的索引
  101. Returns:
  102. 一个字典,包含数据点的特征 (x_data) 和标签 (y_target)
  103. """
  104. # 获取指定索引处的行数据
  105. row = self._target_df.iloc[index]
  106. # 对姓氏进行矢量化
  107. surname_matrix = self._vectorizer.vectorize(row.surname)
  108. # 获取国籍在词汇表中的索引
  109. nationality_index = self._vectorizer.nationality_vocab.lookup_token(row.nationality)
  110. return {'x_surname': surname_matrix,
  111. 'y_nationality': nationality_index}
  112. def get_num_batches(self, batch_size):
  113. """给定批量大小,返回数据集中的批次数
  114. Args:
  115. batch_size (int)
  116. Returns:
  117. 数据集中的批次数
  118. """
  119. # 计算批次数并返回
  120. return len(self) // batch_size
  121. def generate_batches(dataset, batch_size, shuffle=True,
  122. drop_last=True, device="cpu"):
  123. """
  124. 一个生成器函数,用于封装PyTorch的DataLoader。它会确保每个张量都在正确的设备位置上。
  125. Args:
  126. dataset (Dataset): 要生成批次的数据集
  127. batch_size (int): 每个批次的大小
  128. shuffle (bool): 是否在每个epoch开始前打乱数据集,默认为True
  129. drop_last (bool): 如果数据集的大小不能被批次大小整除,是否丢弃最后一个不完整的批次,默认为True
  130. device (str): 张量所在的设备,例如"cpu"或"cuda:0"等,默认为"cpu"
  131. Yields:
  132. 一个字典,包含从数据集中获取的批次数据,并确保每个张量都在指定的设备上
  133. """
  134. # 创建一个PyTorch DataLoader
  135. dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
  136. shuffle=shuffle, drop_last=drop_last)
  137. # 遍历DataLoader中的每个批次
  138. for data_dict in dataloader:
  139. out_data_dict = {}
  140. # 将批次中的每个张量移到指定的设备上
  141. for name, tensor in data_dict.items():
  142. out_data_dict[name] = data_dict[name].to(device)
  143. # 生成经过处理的批次数据
  144. yield out_data_dict

 4.2.2 实例化CNN

相比于MLP中的SurnameClassifier模块,本例使用sequence和ELU PyTorch模块,使用sequeece来封装Conv1d序列的应用程序;ELU是类似于ReLU的非线性函数,但是它不是将值裁剪到0以下,而是对它们求幂:

  1. class SurnameClassifier(nn.Module):
  2. def __init__(self, initial_num_channels, num_classes, num_channels):
  3. """
  4. 初始化方法
  5. Args:
  6. initial_num_channels (int): 输入特征向量的大小
  7. num_classes (int): 输出预测向量的大小
  8. num_channels (int): 网络中使用的恒定通道大小
  9. """
  10. super(SurnameClassifier, self).__init__()
  11. # 定义卷积神经网络模型
  12. self.convnet = nn.Sequential(
  13. nn.Conv1d(in_channels=initial_num_channels,
  14. out_channels=num_channels, kernel_size=3),
  15. nn.ELU(), # 使用RELU作为激活函数
  16. nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
  17. kernel_size=3, stride=2),
  18. nn.ELU(),
  19. nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
  20. kernel_size=3, stride=2),
  21. nn.ELU(),
  22. nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
  23. kernel_size=3),
  24. nn.ELU()
  25. )
  26. # 定义全连接层
  27. self.fc = nn.Linear(num_channels, num_classes)
  28. def forward(self, x_surname, apply_softmax=False):
  29. """分类器的前向传播
  30. Args:
  31. x_surname (torch.Tensor): 输入数据张量。
  32. x_surname.shape 应为 (batch, initial_num_channels, max_surname_length)
  33. apply_softmax (bool): 是否应用softmax激活。
  34. 如果与交叉熵损失一起使用,应为False
  35. Returns:
  36. 结果张量。tensor.shape 应为 (batch, num_classes)
  37. """
  38. # 运行卷积神经网络模型
  39. features = self.convnet(x_surname).squeeze(dim=2)
  40. # 将特征张量传递给全连接层
  41. prediction_vector = self.fc(features)
  42. # 如果需要应用softmax,则应用softmax激活
  43. if apply_softmax:
  44. prediction_vector = F.softmax(prediction_vector, dim=1)
  45. return prediction_vector

CNN实例中的参数设置: 

  1. args = Namespace(
  2. # Data and Path information
  3. surname_csv="data/surnames/surnames_with_splits.csv",
  4. vectorizer_file="vectorizer.json",
  5. model_state_file="model.pth",
  6. save_dir="model_storage/ch4/cnn",
  7. # Model hyper parameters
  8. hidden_dim=100,
  9. num_channels=256,
  10. # Training hyper parameters
  11. seed=1337,
  12. learning_rate=0.001,
  13. batch_size=128,
  14. num_epochs=100,
  15. early_stopping_criteria=5,
  16. dropout_p=0.1,
  17. # Runtime omitted for space ...
  18. )

评估和测试:

  1. def predict_nationality(surname, classifier, vectorizer):
  2. """预测一个新姓氏的国籍
  3. Args:
  4. surname (str): 要分类的姓氏
  5. classifier (SurnameClassifer): 分类器的实例
  6. vectorizer (SurnameVectorizer): 对应的向量化器
  7. Returns:
  8. dict: 包含最可能的国籍及其概率的字典
  9. 'nationality' (str): 预测的国籍
  10. 'probability' (float): 预测的概率值
  11. """
  12. # 使用向量化器将姓氏转换为向量表示
  13. vectorized_surname = vectorizer.vectorize(surname)
  14. # 将向量转换为 PyTorch 的张量,并添加批次维度
  15. vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(0)
  16. # 使用分类器进行预测,应用 softmax 激活函数
  17. result = classifier(vectorized_surname, apply_softmax=True)
  18. # 获取最大概率值及其对应的索引
  19. probability_values, indices = result.max(dim=1)
  20. index = indices.item()
  21. # 使用向量化器的国籍词汇表查询预测的国籍
  22. predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
  23. # 获取预测的概率值
  24. probability_value = probability_values.item()
  25. # 返回预测的国籍及其概率值的字典
  26. return {'nationality': predicted_nationality, 'probability': probability_value}

五、分析总结

        在本次实验中,我们探索了多层感知器(MLP)在多层分类任务中的应用,并深入了解了神经网络不同层次对数据张量大小和形状的影响,同时尝试了带有dropout的模型以观察其对结果的影响。

5.1 多层感知机在多层分类中的应用

      多层感知器是一种经典的前馈神经网络,适用于解决分类问题。在姓氏分类示例中,我们使用了MLP来根据姓氏的字母序列预测姓氏的语言和国家来源。以下是我们从实验中学到的几个关键点:

  • 特征表示与预处理:姓氏作为文本数据,需要转换为数字形式输入到MLP中。我们使用了字符级别的one-hot编码来表示姓氏的每个字符序列,这样神经网络能够理解和处理。

  • 模型架构设计:MLP由多个全连接层组成,每个隐藏层通过激活函数引入非线性,最终输出层使用softmax激活函数生成预测的概率分布。

  • 训练与优化:我们使用了反向传播算法和随机梯度下降(SGD)来优化模型参数,以最小化损失函数(如交叉熵),从而提高分类准确率。

 5.2 神经网络层对数据张量大小和形状的影响

       不同类型的神经网络层(如卷积层、池化层、全连接层等)对输入数据张量的大小和形状有不同的影响:

  • 卷积层:卷积操作保留了输入数据的空间结构,通过滤波器(卷积核)的滑动窗口在输入数据上提取特征。卷积操作会减少输出的空间维度,但增加通道数(深度)。

  • 池化层:池化层通过取局部区域的最大值或平均值来降低数据维度,通常减少输入大小,但保持深度不变。

  • 全连接层:全连接层将前一层的所有神经元与当前层的每个神经元相连,扁平化数据张量,从而影响数据的维度。

 5.3 带有dropout的模型效果分析

       dropout通过在训练过程中随机将一部分神经元置为零来降低神经元之间的依赖性,从而增强了模型的泛化能力。

  • 实验结果:在SurnameClassifier模型中引入dropout后,观察到训练集和验证集上的准确率可能会略有下降,但模型在未见过的数据上的表现更稳定,避免了过拟合。

  • 思考提升:在实际应用中,合理使用dropout可以提升模型的泛化能力,特别是在数据集较小或者复杂度较高时更为有效。需要注意的是,dropout的使用应根据具体任务和数据情况进行调整,避免过度正则化导致欠拟合。

总结

        本次实验通过实际操作和观察,我加深了对多层感知器在多类分类任务中的理解,掌握了神经网络结构与模型构建,同时学习了各项数据处理方式对模型性能的影响。在未来的深度学习任务中,希望进一步探索不同类型的神经网络结构和优化技术,以提升模型的性能和泛化能力。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/838393
推荐阅读
相关标签
  

闽ICP备14008679号