一、实验目的
在本次实验当中我们主要研究的是姓氏分类问题,通过带有多层感知器的姓氏分类,掌握多层感知器在多层分类中的应用,在自然语言处理(NLP)领域中,此问题有着重要的地位,它可以利用姓氏来预测其潜在的国家或者其地区的背景,而因为它涉及将给定的姓氏划分至相应的国家或地区类别,所以它就被划分为分类问题。而在这个实验当中我们将会,采用前馈神经网络来实现这一分类任务,并展示MLP以及卷积神经网络(CNN)如何有效地学习并提取姓氏的特征表示。
二、实验的理论基础
2.1多层感知器(The Multilayer Perceptron)
多层感知器(MLP)被公认为神经网络架构中的基础构建单元。与传统的感知器不同,MLP通过处理数据向量并生成多维输出,实现了功能的扩展。在MLP架构中,感知器被组织成多个层级,每个层级的输出构成了一个新的特征向量,而不仅仅是单一的值。在PyTorch等深度学习框架中,实现这一功能主要依赖于线性层的配置,特别是通过调整输出特征的数量。此外,MLP的显著特性之一在于它融合了多个层级间的非线性激活函数,这些激活函数为网络引入了非线性变换能力,使得MLP能够处理更为复杂的任务。
在图1所示的最基本多层感知器(MLP)结构中,模型由三个主要阶段和两个线性层组成。第一阶段为输入阶段,其中输入向量作为模型的初始数据。以“示例:对餐馆评论的情绪进行分类”为例,输入向量即为Yelp评论的压缩one-hot编码表示。一旦输入向量被接收,第一个线性层便会进行计算,并产生一个隐藏向量,这标志着模型表示的第二阶段。隐藏向量之所以得名,是因为它位于输入与输出层之间,且其值代表了该层中不同神经元(或感知器)的输出。
接下来,利用这个隐藏向量,第二个线性层会进一步计算并输出一个输出向量。在如Yelp评论情感分类这样的二分类任务中,输出向量通常是一个值(例如,概率分数)。然而,在多分类问题中,如在后续“示例:使用多层感知器进行姓氏分类”一节中所探讨的,输出向量的维度将与类别数量相等。
尽管在本例中仅展示了一个隐藏层,但MLP架构可以包含多个中间阶段,每个阶段都会生成其独特的隐藏向量。这些隐藏向量通过线性变换与非线性激活函数的组合,逐步将输入数据映射到最终的输出向量。这样的多层结构使得MLP能够捕捉数据的复杂特征,从而提高模型的预测性能。
图1一种具有两个线性层和三个表示阶段(输入向量、隐藏向量和输出向量)的MLP的可视化表示
多层感知机(MLP)的强大能力,源于其增设的第二线性层,并赋予了模型学习一种中间表征的能力,这种表征可以实现数据的线性划分。具体而言,该模型能描绘出一条直线(或在更高维度中,一个超平面),此界限可有效地对数据点进行分类,依据它们位于该线(或超平面)的哪一侧。这种学习具有特定属性的中间表示方法,例如在分类任务中实现线性可分,深刻体现了神经网络的应用价值,同时也揭示了其建模能力的核心所在。通过这种方法,神经网络能够处理和解析复杂的数据关系,从而实现高效的分类和识别。
2.2卷积神经网络(Convolutional Neural Networks)
卷积神经网络(CNN)是一种卓越的神经网络架构,特别擅长检测并生成有意义的空间子结构。它通过采用有限的权重集来扫描输入数据张量,从而实现了这一功能。在扫描过程中,CNN能够生成输出张量,这些张量代表了对子结构是否存在的检测。CNN的名称和功能基础源于数学中的卷积运算。卷积已在多个工程领域中广泛应用,如数字信号处理和计算机图形学。通常,卷积操作依赖于程序员预设的参数,这些参数是为了实现特定功能而设定的,例如强化边缘或抑制高频噪声。值得注意的是,许多Photoshop滤镜实质上就是应用固定卷积运算到图像上的例子。然而,在深度学习和本项研究中,我们不再依赖预设参数,而是通过数据学习卷积滤波器的参数,以确保这些参数对于当前任务而言是最优的。这种数据驱动的学习方法使CNN能够更好地适应和解决各种复杂问题。
为了深入理解不同设计决策在卷积神经网络(CNN)中的作用,我们在图2中提供了一个直观示例。在这个例子中,一个单独的“卷积核”被用于处理输入矩阵。尽管卷积运算(线性操作)的确切数学公式在此节不是核心焦点,但通过观察该图,我们可以直观地看到卷积核是一个较小的矩阵,它按照一定的规律被应用于输入矩阵的各个位置。这一视觉呈现有助于我们理解卷积核在CNN中的作用。
图2 二维卷积运算
在卷积神经网络(CNN)中,输入矩阵与单个卷积核(亦称特征映射)进行卷积运算,以产生输出矩阵。这个卷积核在输入矩阵的每个位置进行滑动应用。在每个位置,卷积核的数值与输入矩阵对应位置的数值相乘,并将这些乘积求和。卷积核具有一系列超参数配置,如kernel_size设为2,stride设为1,padding设为0,dilation设为1。这些超参数的作用如下:
与经典卷积不同,CNN的设计不依赖于具体设定的卷积核值,而是通过指定一系列超参数来控制卷积操作的行为。这些超参数随后通过梯度下降方法,针对特定数据集进行优化,以找到最佳的卷积核参数。其中,kernel_size决定了卷积核的形状和大小,stride定义了卷积核在输入数据张量上滑动的步长,padding则用于在输入数据张量的边界上填充零,dilation则控制了卷积核内元素之间的间隔。
三、实验过程
3.1对数据的预处理
3.1.1划分数据集
下面代码旨在处理姓氏数据集,首先按照国籍对数据进行分组,随后利用随机数生成器对每个国籍的数据进行随机打乱,并依据预设的比例(训练集70%,验证集15%,测试集15%)将数据集分割为三部分。每部分数据通过添加'split'属性来标识其所属集合(训练、验证或测试)。最终,处理后的数据被整合成一个新的列表,并准备写入CSV文件,以便后续的数据分析和机器学习模型训练。通过这种方法,确保了数据集的有效划分和模型的泛化能力。
import collections
import numpy as np
import pandas as pd
import re
from argparse import Namespace
args = Namespace(
# 原始数据集 CSV 文件路径
raw_dataset_csv="data/surnames/surnames.csv",
# 训练集占比
train_proportion=0.7,
# 验证集占比
val_proportion=0.15,
# 测试集占比
test_proportion=0.15,
# 输出包含数据集划分信息的 CSV 文件路径
output_munged_csv="data/surnames/surnames_with_splits.csv",
# 随机种子
seed=1337
)
# Read raw data
surnames = pd.read_csv(args.raw_dataset_csv, header=0)
# Splitting train by nationality
# Create dict
# 创建一个默认字典 by_nationality
by_nationality = collections.defaultdict(list)
# 使用 collections.defaultdict 创建一个字典,
# 遍历 surnames DataFrame 的每一行
for _, row in surnames.iterrows():
by_nationality[row.nationality].append(row.to_dict())
# 最终得到一个按国籍分组的字典,
# Create split data
# 创建训练、验证和测试集
# 创建一个空列表用于存放最终的数据
final_list = []
# 设置随机种子,确保每次运行结果一致
np.random.seed(args.seed)
# 遍历 by_nationality 字典中的每个国籍
for _, item_list in sorted(by_nationality.items()):
# 对于每个国籍,随机打乱该国籍的数据
np.random.shuffle(item_list)
# 获取该国籍数据的总数
n = len(item_list)
# 根据给定的比例计算训练集、验证集和测试集的大小
n_train = int(args.train_proportion*n)
n_val = int(args.val_proportion*n)
n_test = int(args.test_proportion*n)
# 给每个数据点添加 'split' 属性,标记其所属的集合
for item in item_list[:n_train]:
item['split'] = 'train'
for item in item_list[n_train:n_train+n_val]:
item['split'] = 'val'
for item in item_list[n_train+n_val:]:
item['split'] = 'test'
# 将处理好的数据添加到 final_list 中
final_list.extend(item_list)
# 最终得到一个包含所有数据的列表,每个数据点都有一个 'split' 属性标记其所属的集合
# Write split data to file
# 将 final_list 转换为 pandas DataFrame
final_surnames = pd.DataFrame(final_list)
3.1.2映射处理
下面的代码定义了一个名为 Vocabulary
的类,它的主要目的是提供一种方便的方式来管理和操作文本数据中的词汇表。这个类提供了初始化时可以传入预先存在的词汇到索引的映射字典或从头开始构建词汇表的功能,同时还提供了添加单个词汇或批量添加词汇的方法,并返回对应的索引。此外,它还支持根据词汇查找其索引,或根据索引查找对应词汇的功能,并可以选择返回未知标记的索引或抛出异常。这个类还提供了将 Vocabulary
对象序列化为可序列化的字典,以及从字典中重新创建 Vocabulary
对象的方法,使得词汇表的保存和加载变得更加方便。总的来说,这个 Vocabulary
类是一个用于管理和操作文本数据中词汇表的通用工具,可以为自然语言处理和机器学习任务提供支持
class Vocabulary(object):
"""用于处理文本并提取词汇表以进行映射的类"""
def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
"""
参数:
token_to_idx (dict): 预先存在的词汇到索引的映射
add_unk (bool): 是否添加未知标记
unk_token (str): 要添加到词汇表中的未知标记
"""
if token_to_idx is None:
token_to_idx = {}
self._token_to_idx = token_to_idx
self._idx_to_token = {idx: token
for token, idx in self._token_to_idx.items()}
self._add_unk = add_unk
self._unk_token = unk_token
self.unk_index = -1
if add_unk:
self.unk_index = self.add_token(unk_token)
def to_serializable(self):
"""返回可序列化的字典"""
return {'token_to_idx': self._token_to_idx,
'add_unk': self._add_unk,
'unk_token': self._unk_token}
@classmethod
def from_serializable(cls, contents):
"""从序列化的字典中实例化Vocabulary对象"""
return cls(**contents)
def add_token(self, token):
"""根据词汇更新映射字典
参数:
token (str): 要添加到词汇表的词汇
返回:
index (int): 对应于该词汇的整数索引
"""
try:
index = self._token_to_idx[token]
except KeyError:
index = len(self._token_to_idx)
self._token_to_idx[token] = index
self._idx_to_token[index] = token
return index
def add_many(self, tokens):
"""将一个词汇列表添加到词汇表中
参数:
tokens (list): 一个字符串词汇列表
返回:
indices (list): 对应于这些词汇的索引列表
"""
return [self.add_token(token) for token in tokens]
def lookup_token(self, token):
"""检索与该词汇关联的索引,如果该词汇不存在则返回未知标记的索引
参数:
token (str): 要查找的词汇
返回:
index (int): 对应于该词汇的索引
注意:
`unk_index` 需要 >=0 (已添加到词汇表中)
才能使用未知标记功能
"""
if self.unk_index >= 0:
return self._token_to_idx.get(token, self.unk_index)
else:
return self._token_to_idx[token]
def lookup_index(self, index):
"""返回与给定索引相关联的词汇
参数:
index (int): 要查找的索引
返回:
token (str): 对应于该索引的词汇
抛出:
KeyError: 如果索引不在词汇表中
"""
if index not in self._idx_to_token:
raise KeyError("the index (%d) is not in the Vocabulary" % index)
return self._idx_to_token[index]
def __str__(self):
return "<Vocabulary(size=%d)>" % len(self)
def __len__(self):
return len(self._token_to_idx)
3.2one-hot编码
这段代码定义了一个名为SurnameVectorizer
的类,它的主要目的是将姓氏和国籍转换为数值向量,以便在机器学习模型中使用。SurnameVectorizer
类通过两个词汇表(surname_vocab
和nationality_vocab
)来实现这个功能,这两个词汇表分别用于将姓氏中的字符和国籍转换为唯一的整数索引。
vectorize
方法负责将给定的姓氏字符串转换为一个one-hot编码的向量。这个向量中的每个位置对应于词汇表中的一个字符,如果该字符出现在姓氏中,则对应位置的值为1,否则为0。
from_dataframe
和from_serializable
是类方法,分别用于从数据集数据帧和可序列化的字典中实例化SurnameVectorizer
对象。from_dataframe
方法通过遍历数据集数据帧来构建姓氏和国籍的词汇表,而from_serializable
方法则用于从先前保存的序列化词汇表中恢复SurnameVectorizer
对象。
to_serializable
方法用于将SurnameVectorizer
对象转换为可序列化的字典格式,以便可以将其保存到文件或数据库中。这样,在其他地方加载并重新使用SurnameVectorizer
对象时,就可以确保使用相同的词汇表来转换数据。
class SurnameVectorizer(object):
"""用于协调 Vocabularies 并将它们用于实际应用的向量化器"""
def __init__(self, surname_vocab, nationality_vocab):
"""
初始化方法
参数:
surname_vocab (Vocabulary): 将字符映射到整数的词汇表
nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
"""
self.surname_vocab = surname_vocab
self.nationality_vocab = nationality_vocab
def vectorize(self, surname):
"""
将姓氏转换为向量表示
参数:
surname (str): 要转换的姓氏
返回:
one_hot (np.ndarray): 一个折叠的one-hot编码
"""
vocab = self.surname_vocab
one_hot = np.zeros(len(vocab), dtype=np.float32)
for token in surname:
one_hot[vocab.lookup_token(token)] = 1
return one_hot
@classmethod
def from_dataframe(cls, surname_df):
"""从数据集数据帧中实例化向量化器
参数:
surname_df (pandas.DataFrame): 姓氏数据集
返回:
SurnameVectorizer 的实例
"""
surname_vocab = Vocabulary(unk_token="@")
nationality_vocab = Vocabulary(add_unk=False)
for index, row in surname_df.iterrows():
for letter in row.surname:
surname_vocab.add_token(letter)
nationality_vocab.add_token(row.nationality)
return cls(surname_vocab, nationality_vocab)
@classmethod
def from_serializable(cls, contents):
"""从可序列化的字典中实例化向量化器"""
surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
nationality_vocab = Vocabulary.from_serializable(contents['nationality_vocab'])
return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab)
def to_serializable(self):
"""将向量化器转换为可序列化的字典"""
return {'surname_vocab': self.surname_vocab.to_serializable(),
'nationality_vocab': self.nationality_vocab.to_serializable()}
class SurnameVectorizer(object):
""" 负责协调 Vocabularies 并将其用于实际应用的向量化器 """
def __init__(self, surname_vocab, nationality_vocab, max_surname_length):
"""
参数:
surname_vocab (Vocabulary): 将字符映射到整数的词汇表
nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
max_surname_length (int): 最长姓氏的长度
"""
self.surname_vocab = surname_vocab
self.nationality_vocab = nationality_vocab
self._max_surname_length = max_surname_length
def vectorize(self, surname):
"""
参数:
surname (str): 姓氏
返回:
one_hot_matrix (np.ndarray): 一个由one-hot向量组成的矩阵
"""
one_hot_matrix_size = (len(self.surname_vocab), self._max_surname_length)
one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)
for position_index, character in enumerate(surname):
character_index = self.surname_vocab.lookup_token(character)
one_hot_matrix[character_index][position_index] = 1
return one_hot_matrix
@classmethod
def from_dataframe(cls, surname_df):
"""从数据集数据帧中实例化向量化器
参数:
surname_df (pandas.DataFrame): 姓氏数据集
返回:
SurnameVectorizer 的一个实例
"""
surname_vocab = Vocabulary(unk_token="@")
nationality_vocab = Vocabulary(add_unk=False)
max_surname_length = 0
for index, row in surname_df.iterrows():
max_surname_length = max(max_surname_length, len(row.surname))
for letter in row.surname:
surname_vocab.add_token(letter)
nationality_vocab.add_token(row.nationality)
return cls(surname_vocab, nationality_vocab, max_surname_length)
@classmethod
def from_serializable(cls, contents):
surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
nationality_vocab = Vocabulary.from_serializable(contents['nationality_vocab'])
return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab,
max_surname_length=contents['max_surname_length'])
def to_serializable(self):
return {'surname_vocab': self.surname_vocab.to_serializable(),
'nationality_vocab': self.nationality_vocab.to_serializable(),
'max_surname_length': self._max_surname_length}
3.3初始化输入
这段代码定义了一个名为SurnameDataset
的类,它继承自Dataset
(通常指的是PyTorch的torch.utils.data.Dataset
)。这个类的目的是为机器学习模型提供姓氏和国籍的数据集。在__getitem__
方法中,它根据提供的索引从内部的DataFrame(_target_df
)中获取相应的行。然后,它使用_vectorizer
对象的vectorize
方法将姓氏转换为数值向量(这里是one-hot编码)。接着,它查找国籍在国籍词汇表(nationality_vocab
)中的索引。最后,它返回一个字典,包含姓氏的向量表示(x_surname
)和国籍的索引(y_nationality
),这些可以作为模型的输入和标签使用。这样,SurnameDataset
类为机器学习模型提供了结构化的数据输入方式。
class SurnameDataset(Dataset):
def __getitem__(self, index):
# 使用iloc从DataFrame中获取指定索引的行
row = self._target_df.iloc[index]
# 使用Vectorizer的vectorize方法将姓氏转换为向量表示
surname_vector = self._vectorizer.vectorize(row.surname)
# 使用Vectorizer的nationality_vocab的lookup_token方法查找国籍的索引
nationality_index = self._vectorizer.nationality_vocab.lookup_token(row.nationality)
# 返回一个包含姓氏向量和国籍索引的字典
return {'x_surname': surname_vector,
'y_nationality': nationality_index}
3.4构建模型
在本次实验当中我们可以直接使用PyTorch提供的神经网络模型框架
3.4.1mlp
这段代码定义了一个名为SurnameClassifier
的神经网络模型,用于姓氏分类。它包含两个全连接层,一个ReLU激活函数,并可选择应用Softmax函数。模型通过前向传播方法接收输入数据并输出预测结果。
class SurnameClassifier(nn.Module):
""" 一个两层的多层感知机,用于姓氏分类 """
def __init__(self, input_dim, hidden_dim, output_dim):
"""
Args:
input_dim (int): 输入向量的维度
hidden_dim (int): 第一层全连接层的输出维度
output_dim (int): 第二层全连接层的输出维度(通常与分类数量相等)
"""
super(SurnameClassifier, self).__init__() # 调用父类nn.Module的初始化方法
self.fc1 = nn.Linear(input_dim, hidden_dim) # 定义一个全连接层,输入维度为input_dim,输出维度为hidden_dim
self.fc2 = nn.Linear(hidden_dim, output_dim)# 定义一个全连接层,输入维度为hidden_dim,输出维度为output_dim
def forward(self, x_in, apply_softmax=False):
""" 分类器的前向传播
Args:
x_in (torch.Tensor): 输入数据张量。x_in.shape 应该是 (batch_size, input_dim)
apply_softmax (bool): 是否应用Softmax激活函数的标志
如果使用交叉熵损失函数,则应该设置为False
Returns:
预测结果张量。tensor.shape 应该是 (batch_size, output_dim)
"""
intermediate_vector = F.relu(self.fc1(x_in)) # 通过第一层全连接层,并应用ReLU激活函数
prediction_vector = self.fc2(intermediate_vector) # 通过第二层全连接层,得到最终的预测向量
if apply_softmax:
prediction_vector = F.softmax(prediction_vector, dim=1) # 如果apply_softmax为True,则对预测向量应用Softmax激活函数
return prediction_vector# 返回预测向量
3.4.2cnn
SurnameClassifier
是一个基于CNN的模型,处理输入形状为(batch, initial_num_channels, max_surname_length)
的数据。模型包含四个卷积层,其中第二、三层设置步长为2以减小特征图尺寸。每个卷积层后可能跟随激活函数。最终,特征图通过展平或池化操作传递给全连接层,生成(batch, num_classes)
的预测向量。在模型设计时,需要确保卷积层的参数配置能够使得特征图的宽度在传递到全连接层前达到合适的尺寸。
class SurnameClassifier(nn.Module):
def __init__(self, initial_num_channels, num_classes, num_channels):
"""
初始化 SurnameClassifier 神经网络。
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:
torch.Tensor: 结果张量。tensor.shape 应为 (batch, num_classes)
"""
# 通过卷积层传递输入
features = self.convnet(x_surname).squeeze(dim=2)
# 通过全连接层传递特征
prediction_vector = self.fc(features)
# 如果指定了 apply_softmax,则应用 softmax 激活
if apply_softmax:
prediction_vector = F.softmax(prediction_vector, dim=1)
return prediction_vector
3.5训练模型
进行神经网络模型训练流程,包括数据准备、模型定义、优化器设置以及训练和验证的循环过程
- 将模型和数据移动到GPU设备上(如果可用)。
- 定义损失函数、优化器和学习率调度器。
- 创建训练状态,并设置训练和验证数据集。
- 进行训练循环,包括:
- 在训练集上进行前向传播、计算损失、反向传播更新参数。
- 在验证集上进行评估,记录验证集损失和准确率。
- 根据验证集性能更新训练状态和学习率。
- 使用进度条显示训练过程。
- 添加异常处理,允许用户中断训练
# 将分类器移至GPU(如果可用)
classifier = classifier.to(args.device)
# 将类权重移至GPU(如果可用)
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
# 遍历训练数据集
# 设置: batch生成器, 将损失和准确率初始化为0, 设置为训练模式
dataset.set_split('train')
batch_generator = generate_batches(dataset,
batch_size=args.batch_size,
device=args.device)
running_loss = 0.0
running_acc = 0.0
classifier.train()
for batch_index, batch_dict in enumerate(batch_generator):
# 训练流程的5个步骤:
# 1. 梯度清零
optimizer.zero_grad()
# 2. 计算模型输出
y_pred = classifier(batch_dict['x_surname'])
# 3. 计算损失
loss = loss_func(y_pred, batch_dict['y_nationality'])
loss_t = loss.item()
running_loss += (loss_t - running_loss) / (batch_index + 1)
# 4. 反向传播计算梯度
loss.backward()
# 5. 使用优化器更新参数
optimizer.step()
# 计算训练批次的准确率
acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
running_acc += (acc_t - running_acc) / (batch_index + 1)
# 更新训练进度条
train_bar.set_postfix(loss=running_loss, acc=running_acc,
epoch=epoch_index)
train_bar.update()
# 记录当前epoch的训练损失和准确率
train_state['train_loss'].append(running_loss)
train_state['train_acc'].append(running_acc)
# 遍历验证数据集
# 设置: batch生成器, 将损失和准确率初始化为0, 设置为评估模式
dataset.set_split('val')
batch_generator = generate_batches(dataset,
batch_size=args.batch_size,
device=args.device)
running_loss = 0.
running_acc = 0.
classifier.eval()
for batch_index, batch_dict in enumerate(batch_generator):
# 计算模型输出
y_pred = classifier(batch_dict['x_surname'])
# 计算验证损失
loss = loss_func(y_pred, batch_dict['y_nationality'])
loss_t = loss.item()
running_loss += (loss_t - running_loss) / (batch_index + 1)
# 计算验证准确率
acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
running_acc += (acc_t - running_acc) / (batch_index + 1)
val_bar.set_postfix(loss=running_loss, acc=running_acc,
epoch=epoch_index)
val_bar.update()
# 记录当前epoch的验证损失和准确率
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])
# 重置训练和验证进度条, 更新总体训练进度条
train_bar.n = 0
val_bar.n = 0
epoch_bar.update()
except KeyboardInterrupt:
print("Exiting loop")
图4训练模型
3.6相关测试
利用上面训练的模型,进行姓氏分类。
def predict_topk_nationality(name, classifier, vectorizer, k=5):
# 向量化输入的姓氏
vectorized_name = vectorizer.vectorize(name)
vectorized_name = torch.tensor(vectorized_name).view(1, -1)
# 使用分类器进行预测,并对结果应用 softmax 函数
prediction_vector = classifier(vectorized_name, apply_softmax=True)
# 获取前 k 个最高概率的预测值和对应的索引
probability_values, indices = torch.topk(prediction_vector, k=k)
# 转换为 numpy 数组以便处理
probability_values = probability_values.detach().numpy()[0]
indices = indices.detach().numpy()[0]
results = []
# 对每个预测值和索引进行处理
for prob_value, index in zip(probability_values, indices):
# 从索引获取国籍并添加到结果中
nationality = vectorizer.nationality_vocab.lookup_index(index)
results.append({'nationality': nationality,
'probability': prob_value})
return results
new_surname = input("Enter a surname to classify: ")
# 将分类器移到 CPU 上
classifier = classifier.to("cpu")
k = int(input("How many of the top predictions to see? "))
# 如果用户要求的预测数量大于国籍词汇表的大小,则给出提示并将 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)
print("Top {} predictions:".format(k))
print("===================")
# 打印预测结果
for prediction in predictions:
print("{} -> {} (p={:0.2f})".format(new_surname,
prediction['nationality'],
prediction['probability']))
图5对于姓氏mlp预测最可能的前五个国籍
图6对于姓氏CNN预测最可能的前五个国籍
四、实验总结
本次实验让我对使用前馈神经网络(MLP与CNN)实现姓氏分类有了更深入的理解和实践。在模型设计方面,MLP模型由几个全连接层组成,输入为姓氏的数字向量表示,输出为国籍预测结果。CNN模型则利用卷积层提取姓氏的局部特征,并通过池化层进行特征聚合,最终输出国籍预测。通过本次实验,我更加清晰地认识到在MLP与CNN实际应用中的价值和意义。总的来说,这次实验是一次宝贵的学习经历,让我受益匪浅。不过本篇构建的MLP和CNN都相对简单,对于这两种神经网络的讲解也并未深入,感兴趣的朋友可以自行学习。