一、感知机
感知器,也可翻译为感知机,是Frank Rosenblatt在1957年就职于Cornell航空实验室(Cornell Aeronautical Laboratory)时所发明的一种人工神经网络。它可以被视为一种最简单形式的前馈式人工神经网络,是一种二元线性分类器。
在人工神经网络领域中,感知器也被指为单层的人工神经网络,以区别于较复杂的多层感知器(Multilayer Perceptron)。 作为一种线性分类器,(单层)感知器可说是最简单的前向人工神经网络形式。尽管结构简单,感知器能够学习并解决相当复杂的问题。感知器主要的本质缺陷是它不能处理线性不可分问题。
例如,下图绘制的数据点。这相当于一个二分类问题,但是在这种情况下,决策边界不是一条直线(也就是说不是线性可分的),所以感知器无法成功地对此进行分类。
二、多层感知机
对于上述问题,我们可以采用多层感知机与卷积神经网络这两种前馈神经网络来解决。
2.1多层感知机简介
多层感知器(MLP)被认为是最基本的神经网络构建模块之一。最简单的MLP是对感知器的扩展。感知器将数据向量作为输入,计算出一个输出值。在MLP中,许多感知器被分组,以便单个层的输出是一个新的向量,而不是单个输出值。
共可分为三个阶段,第一阶段是输入向量。这是给定给模型的向量。给定输入向量,第一个线性层计算一个隐藏向量——表示的第二阶段。隐藏向量之所以这样被调用,是因为它是位于输入和输出之间的层的输出。使用这个隐藏的向量,第二个线性层计算一个输出向量。在像Yelp评论分类这样的二进制任务中,输出向量仍然可以是1。在多类设置中,将在本实验后面的“示例:带有多层感知器的姓氏分类”一节中看到,输出向量是类数量的大小。虽然在这个例子中,我们只展示了一个隐藏的向量,但是有可能有多个中间阶段,每个阶段产生自己的隐藏向量。最终的隐藏向量总是通过线性层和非线性的组合映射到输出向量。
2.2多层感知机与感知机区别样例
在下图所示例子中,我们在一个二元分类任务中训练感知器和MLP:星和圆。每个数据点是一个二维坐标。在不深入研究实现细节的情况下,最终的模型预测如图所示。在这个图中,错误分类的数据点用黑色填充,而正确分类的数据点没有填充。在左边的面板中,从填充的形状可以看出,感知器在学习一个可以将星星和圆分开的决策边界方面有困难。然而,MLP(右面板)学习了一个更精确地对星星和圆进行分类的决策边界。
在左边的面板中,感知器学习了—个不能正确地将圆与星分开的决策边界。然而,在右动的面板中,MLP学会了从圆中分离星。
从左到右:(1)网络的输入;(2)第一个线性模块的输出;(3)第一个非线性模块的输出;(4)第二个线性模块的输出。
可见多层感知机先通过第一个线性模块将圆和星分组,再通过第二个线性模块将数据点重新变得线性可分
而感知器没有额外的一层来处理数据的形状,所以它无法将圆和星星分开
2.3在PyTorch中实现MLPs
class MultilayerPerceptron(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
"""
Args:
input_dim (int): 输入向量的尺寸
hidden_dim (int): 第一个线性层的输出大小
output_dim (int): 第二个线性层的输出大小
"""
super(MultilayerPerceptron, self).__init__()
# 定义第一个全连接层
self.fc1 = nn.Linear(input_dim, hidden_dim)
# 定义第二个全连接层
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x_in, apply_softmax=False):
"""MLP的前向传播
Args:
x_in (torch.Tensor):输入数据张量
x_in.shape should be (batch, input_dim)
apply_softmax (bool):一个标志,表示是否应用 softmax 激活函数
如果与交叉熵损失一起使用,则应为 False
Returns:
结果张量 tensor.shape should be (batch, output_dim)
"""
#第一个全连接层的前向传播,并使用ReLU激活函数
intermediate = F.relu(self.fc1(x_in))
#第二个全连接层的前向传播
output = self.fc2(intermediate)
#如果应用softmax激活函数,则对输出进行softmax处理
if apply_softmax:
output = F.softmax(output, dim=1)
return output
batch_size = 2 #一次输入的样本数
input_dim = 3 #输入维度
hidden_dim = 100#隐藏层维度
output_dim = 4 #输出维度
#初始化模型
mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
print(mlp)#打印模型结构
首先对MLP进行实例化。由于MLP实现的通用性,可以为任何大小的输入建模。为了演示,我们使用大小为3的输入维度、大小为4的输出维度和大小为100的隐藏维度。
2.4示例:使用多层感知器进行姓氏分类
2.4.1姓氏数据集
姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,这些姓氏是作者从互联网上不同的姓名来源收集的。该数据集将在本课程实验的几个示例中重用,并具有一些使其有趣的属性。第一个性质是它是相当不平衡的。排名前三的课程占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。剩下的15个民族的频率也在下降——这也是语言特有的特性。第二个特点是,在国籍和姓氏正字法(拼写)之间有一种有效和直观的关系。有些拼写变体与原籍国联系非常紧密(比如“O ‘Neill”、“Antonopoulos”、“Nagasawa”或“Zhu”)。
为了创建最终的数据集,我们需要对原始数据集进行修改。第一个目的是减少原始数据中俄文占比过大的不平衡,我们通过选择标记为俄语的姓氏的随机子集对这个过度代表的类进行子样本。接下来,我们根据国籍对数据集进行分组,并将数据集分为三个部分:70%为训练数据集,15%为验证数据集,最后15%为测试数据集,以便跨这些部分的类标签分布具有可比性。
下面的代码定义了SurnameDataset类别,在该类别中我们需要实现两个函数:__getitem
方法和len方法。__getitem
方法在给定索引时返回一个数据点;len方法返回数据集的长度。
class SurnameDataset(Dataset):
def __init__(self, surname_df, vectorizer):
#初始化函数
"""
Args:
name_df (pandas.DataFrame):数据集
vectorizer (SurnameVectorizer):从数据集实例化的矢量化器
"""
#设置数据集和矢量化器
self.surname_df = surname_df
self._vectorizer = vectorizer
#将数据集按照'train'进行划分,并获取划分后的大小
self.train_df = self.surname_df[self.surname_df.split=='train']
self.train_size = len(self.train_df)
#将数据集按照'val'进行划分,并获取划分后的大小
self.val_df = self.surname_df[self.surname_df.split=='val']
self.validation_size = len(self.val_df)
#将数据集按照'test'进行划分,并获取划分后的大小
self.test_df = self.surname_df[self.surname_df.split=='test']
self.test_size = len(self.test_df)
#将划分后的数据集及其大小存储在字典中
self._lookup_dict = {'train': (self.train_df, self.train_size),
'val': (self.val_df, self.validation_size),
'test': (self.test_df, self.test_size)}
#设置默认的划分为'train'
self.set_split('train')
#类别权重
class_counts = surname_df.nationality.value_counts().to_dict()#统计每个国籍的样本数量并转化为字典
def sort_key(item):#定义排序键函数
return self._vectorizer.nationality_vocab.lookup_token(item[0]) #返回国籍的索引
sorted_counts = sorted(class_counts.items(), key=sort_key)#按照国籍索引对类别进行排序
frequencies = [count for _, count in sorted_counts]#提取排序后的类别数量
self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)#计算类别权重
@classmethod
def load_dataset_and_make_vectorizer(cls, surname_csv):
"""加载数据集并从头创建一个新的矢量化器
Args:
surname_csv (str):数据集的位置
Returns:
一个SurnameDataset实例
"""
#读取姓氏数据集
surname_df = pd.read_csv(surname_csv)
#从数据集中获取训练集的数据
train_surname_df = surname_df[surname_df.split=='train']
# 实例化一个新的矢量化器
return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
@classmethod
def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
"""加载数据集和对应的矢量化器。
用于在矢量化器已被缓存以便重复使用的情况下
Args:
surname_csv (str): 数据集的位置
vectorizer_filepath (str): 已保存的矢量化器的位置
Returns:
一个SurnameDataset实例
"""
#读取姓氏数据集
surname_df = pd.read_csv(surname_csv)
#加载已保存的矢量化器
vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
#返回加载的数据集和矢量化器的实例
return cls(surname_df, vectorizer)
@staticmethod
def load_vectorizer_only(vectorizer_filepath):
"""从文件加载矢量化器的静态方法
Args:
vectorizer_filepath (str): 序列化矢量化器的位置
Returns:
一个SurnameDataset实例
"""
#打开文件并加载序列化的矢量化器
with open(vectorizer_filepath) as fp:
return SurnameVectorizer.from_serializable(json.load(fp))
def save_vectorizer(self, vectorizer_filepath):
"""使用json将矢量化器保存到磁盘
Args:
vectorizer_filepath (str): 保存矢量化器的位置
"""
#将矢量化器序列化并写入文件
with open(vectorizer_filepath, "w") as fp:
json.dump(self._vectorizer.to_serializable(), fp)
def get_vectorizer(self):
""" 返回矢量化器 """
return self._vectorizer
def set_split(self, split="train"):
"""使用数据框中的列选择数据集中的拆分部分"""
#设置目标拆分部分
self._target_split = split
#根据目标拆分部分设置目标数据框和大小
self._target_df, self._target_size = self._lookup_dict[split]
def __len__(self):
#返回数据集的大小
return self._target_size
def __getitem__(self, index):
"""PyTorch 数据集的主要入口方法
Args:
index (int): 数据点的索引
Returns:
一个字典,其中包含数据点的特征(x_data)和标签(y_target)
"""
#获取指定索引处的行数据
row = self._target_df.iloc[index]
#使用矢量化器将姓氏向量化
surname_matrix = \
self._vectorizer.vectorize(row.surname)
#查找国籍对应的索引
nationality_index = \
self._vectorizer.nationality_vocab.lookup_token(row.nationality)
#返回数据点的特征和标签
return {'x_surname': surname_matrix,
'y_nationality': nationality_index}
def get_num_batches(self, batch_size):
"""给定批次大小,返回数据集中的批次数量
Args:
batch_size (int):批次大小
Returns:
数据集中的批次数量
"""
return len(self) // batch_size
def generate_batches(dataset, batch_size, shuffle=True,
drop_last=True, device="cpu"):
"""
一个生成器函数,用于包装 PyTorch DataLoader。它将确保每个张量位于正确的设备位置上。
"""
#创建一个 PyTorch DataLoader 实例
dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
shuffle=shuffle, drop_last=drop_last)
#遍历数据加载器中的每个数据字典
for data_dict in dataloader:
#初始化输出数据字典
out_data_dict = {}
#遍历数据字典中的每个名称和张量
for name, tensor in data_dict.items():
#将张量移动到指定设备上,并存储到输出数据字典中
out_data_dict[name] = data_dict[name].to(device)
#生成输出数据字典
yield out_data_dict
2.4.2词汇表,向量化器, DataLoader
为了使用字符对姓氏进行分类,我们使用词汇表(Vocabulary)、向量化器( Vectorizer)和DataLoader将姓氏字符串转换为向量化的minibatches。
THE VOCABULARY CLASS
词汇表是两个Python字典的协调,这两个字典实现了先将字符映射到整数索引,再从整数索引映射到字符上。add_token方法用于向词汇表中添加新的字符,lookup_token方法用于检索索引,lookup_index方法用于检索给定索引的字符(在推断阶段很有用)。使用的是one-hot词汇表,不计算字符出现的频率,只对频繁出现的条目进行限制。这主要是因为数据集很小,而且大多数字符足够频繁。
class Vocabulary(object):
"""Class to process text and extract vocabulary for mapping"""
def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
"""
Args:
token_to_idx (dict):一个已有的将词汇映射到索引的字典
add_unk (bool):一个是否添加UNK(未知)标记的标志
unk_token (str):要添加到词汇表中的UNK标记
"""
#如果token_to_idx为None,则初始化为空字典
if token_to_idx is None:
token_to_idx = {}
#设置_token_to_idx属性为传入的token_to_idx
self._token_to_idx = token_to_idx
#使用_token_to_idx创建_idx_to_token字典,反转键值对
self._idx_to_token = {idx: token
for token, idx in self._token_to_idx.items()}
#初始化_add_unk和_unk_token属性
self._add_unk = add_unk
self._unk_token = unk_token
#初始化unk_index为-1
self.unk_index = -1
#如果add_unk为True,则调用add_token方法添加unk_token,并将unk_index设置为其索引
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更新映射字典
Args:
token (str):要添加到词汇表中的项目
Returns:
index (int):对应于token的整数索引
"""
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):
"""将一组token添加到词汇表中
Args:
tokens (list):字符串token的列表
Returns:
indices (list):对应于tokens的索引列表
"""
return [self.add_token(token) for token in tokens]
def lookup_token(self, token):
"""检索与token关联的索引或者如果token不存在则返回UNK索引
Args:
token (str):要查找的token
Returns:
index (int): 对应于token的索引
Notes:
UNK功能需要 `unk_index` 大于等于0(已添加到词汇表中)
"""
if self.unk_index >= 0:
return self._token_to_idx.get(token, self.unk_index)#如果token存在,则返回对应的索引;否则返回unk_index
else:
return self._token_to_idx[token]#如果unk_index未设置为有效值,则直接返回token的索引
def lookup_index(self, index):
"""返回与索引关联的token
Args:
index (int):要查找的索引
Returns:
token (str):对应于索引的token
Raises:
KeyError: 如果索引不在词汇表中
"""
if index not in self._idx_to_token:
raise KeyError("the index (%d) is not in the Vocabulary" % index) #如果索引不存在于词汇表中,则引发KeyError异常
return self._idx_to_token[index] #返回对应于索引的token
def __str__(self):
return "<Vocabulary(size=%d)>" % len(self)
def __len__(self):
return len(self._token_to_idx)
THE SURNAMEVECTORIZER
SurnameVectorizer负责应用词汇表并将姓氏转换为向量。姓氏是字符的序列,每个字符在我们的词汇表中是一个单独的标记。在该例子中使用了收缩的one-hot,
class SurnameVectorizer(object):
"""协调词汇表并将其应用的矢量化器"""
def __init__(self, surname_vocab, nationality_vocab):
self.surname_vocab = surname_vocab#姓氏词汇表
self.nationality_vocab = nationality_vocab#国籍词汇表
#将提供的姓氏转换为向量表示
def vectorize(self, surname):
"""Vectorize the provided surname
Args:
surname (str): 姓氏
Returns:
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):
""" 从数据框中实例化矢量化器
Args:
surname_df (pandas.DataFrame):姓氏数据集
Returns:
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)
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()}
2.4.3姓氏分类器模型
姓氏分类器模型是多层感知机的实现,第一个线性层将输入向量映射到中间向量,并对该向量应用非线性。第二线性层将中间向量映射到预测向量。在最后一步中,可选择是否应用softmax操作,以确保输出和为1。
import torch.nn as nn
import torch.nn.functional as F
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__()
self.fc1 = nn.Linear(input_dim, hidden_dim)#第一个全连接层
self.fc2 = nn.Linear(hidden_dim, output_dim)#第二个全连接层
def forward(self, x_in, apply_softmax=False):
"""分类器的前向传播
Args:
x_in (torch.Tensor):输入数据张量
x_in.shape should be (batch, input_dim)
apply_softmax (bool): softmax 激活的标志
如果与交叉熵损失一起使用,则应为 false
the resulting tensor. tensor.shape should be (batch, 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)#应用softmax函数
return prediction_vector
2.4.4The Training Routine
训练过程如下
from argparse import Namespace
import os
import pandas as pd
import numpy as np
import collections
import json
def set_seed_everywhere(seed, cuda):
#设置numpy随机种子
np.random.seed(seed)
#设置PyTorch随机种子
torch.manual_seed(seed)
#如果使用CUDA,则设置所有CUDA设备的随机种子
if cuda:
torch.cuda.manual_seed_all(seed)
def handle_dirs(dirpath):
#如果目录不存在,则创建目录
if not os.path.exists(dirpath):
os.makedirs(dirpath)
args = Namespace(
#数据和路径信息
surname_csv="data/surnames/surnames_with_splits.csv",
vectorizer_file="vectorizer.json",#保存路径
model_state_file="model.pth",#模型状态文件的保存路径
save_dir="model_storage/ch4/surname_mlp",#模型保存目录
#模型超参数
hidden_dim=300,#隐藏层维度
#训练超参数
seed=1337,#随机种子
num_epochs=100,#训练轮数
early_stopping_criteria=5,#提前停止的条件
learning_rate=0.001,#学习率
batch_size=64,#批量大小
# Runtime options omitted for space
cuda=False,
reload_from_files=False,
expand_filepaths_to_save_dir=True,
catch_keyboard_interrupt=True,
raw_dataset_csv="surnames.csv",
train_proportion=0.7,
val_proportion=0.15,
num_channels=256,
test_proportion=0.15,
output_munged_csv="data/surnames/surnames_with_splits.csv",
)
#从CSV文件中读取姓氏数据集
surnames = pd.read_csv(args.raw_dataset_csv, header=0)
#创建一个默认字典,按国籍分组存储数据
by_nationality = collections.defaultdict(list)
for _, row in surnames.iterrows():
by_nationality[row.nationality].append(row.to_dict())
#初始化最终的数据列表
final_list = []
#设置随机种子
np.random.seed(args.seed)
#遍历按国籍分组后的数据项
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)
#为每个数据点添加一个分割属性
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.extend(item_list)
#创建最终的姓氏数据框架
final_surnames = pd.DataFrame(final_list)
#将最终的数据框架保存为CSV文件
final_surnames.to_csv(args.output_munged_csv, index=False)
#如果需要,将文件路径扩展为保存目录中的路径
if args.expand_filepaths_to_save_dir:
#将向量化器文件路径扩展为保存目录中的路径
args.vectorizer_file = os.path.join(args.save_dir,
args.vectorizer_file)
#将模型状态文件路径扩展为保存目录中的路径
args.model_state_file = os.path.join(args.save_dir,
args.model_state_file)
#打印扩展后的文件路径
print("Expanded filepaths: ")
print("\t{}".format(args.vectorizer_file))
print("\t{}".format(args.model_state_file))
#检查是否支持 CUDA
if not torch.cuda.is_available():
args.cuda = False
#根据是否支持CUDA设置设备
args.device = torch.device("cuda" if args.cuda else "cpu")
#打印是否使用 CUDA
print("Using CUDA: {}".format(args.cuda))
#为了保证可重复性,设置随机种子
set_seed_everywhere(args.seed, args.cuda)
#处理目录,确保保存目录存在
handle_dirs(args.save_dir)
if args.reload_from_files:
#从检查点重新加载训练
print("Reloading!")
dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
args.vectorizer_file)
else:
#创建新的数据集和词向量化器
print("Creating fresh!")
dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
dataset.save_vectorizer(args.vectorizer_file)
#获取词向量化器
vectorizer = dataset.get_vectorizer()
#创建姓氏分类器
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab),
hidden_dim=args.hidden_dim,
output_dim=len(vectorizer.nationality_vocab))
本示例中的输出是一个多类预测向量,可以转换为概率。正如在模型描述中所描述的,这种输出的损失类型仅限于CrossEntropyLoss和NLLLoss。由于它的简化,我们使用CrossEntropyLoss。
数据集、模型、损失函数和优化器的实例化
import pandas as pd
import torch.optim as optim
#从CSV文件加载数据集并创建词向量化器
dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
vectorizer = dataset.get_vectorizer()#获取数据集的词向量化器
#创建姓氏分类器,指定输入维度为姓氏词汇表的大小,隐藏层维度为指定的 args.hidden_dim,输出维度为国籍词汇表的大小
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab),
hidden_dim=args.hidden_dim,
output_dim=len(vectorizer.nationality_vocab))
#将分类器移动到指定的设备上
classifier = classifier.to(args.device)
#定义损失函数为交叉熵损失函数,使用数据集中定义的类权重
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
#定义优化器为 Adam 优化器,优化分类器的参数,学习率为指定的 args.learning_rate
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
训练循环
以下代码展示了使用不同的key从batch_dict中获取数据。除了外观上的差异,训练循环的功能保持不变。利用训练数据,计算模型输出、损失和梯度。最后使用梯度来更新模型。
from tqdm import tqdm_notebook
from torch.utils.data import Dataset, DataLoader
def make_train_state(args):
return {'stop_early': False, #是否提前停止训练的标志,初始为 False
'early_stopping_step': 0, #早停步数计数器,初始为 0
'early_stopping_best_val': 1e8, #最佳验证集损失的初始值,设为一个很大的数
'learning_rate': args.learning_rate, #记录当前的学习率
'epoch_index': 0, #当前训练的轮数索引,初始为 0
'train_loss': [], #用于存储每轮训练集的损失值
'train_acc': [], #用于存储每轮训练集的准确率
'val_loss': [], #用于存储每轮验证集的损失值
'val_acc': [], #用于存储每轮验证集的准确率
'test_loss': -1, #测试集损失的初始值,设为 -1 表示尚未计算
'test_acc': -1, #测试集准确率的初始值,设为 -1 表示尚未计算
'model_filename': args.model_state_file #模型文件名,用于保存训练状态
}
def compute_accuracy(y_pred, y_target):
_, y_pred_indices = y_pred.max(dim=1)#获取预测结果中每行最大值的索引
n_correct = torch.eq(y_pred_indices, y_target).sum().item()#计算预测正确的样本数量
return n_correct / len(y_pred_indices) * 100#返回准确率,以百分比表示
def update_train_state(args, model, train_state):
"""处理训练状态的更新
Components:
- 提前停止: 防止过拟合
- 模型检查点: 如果模型性能更好则保存模型
:param args: 主要参数
:param model: 要训练的模型
:param train_state:表示训练状态值的字典
:returns:
a new train_state
"""
#至少保存一个模型
if train_state['epoch_index'] == 0:
torch.save(model.state_dict(), train_state['model_filename'])
train_state['stop_early'] = False
#如果模型经过至少一次迭代
elif train_state['epoch_index'] >= 1:
loss_tm1, loss_t = train_state['val_loss'][-2:]
#如果损失变差了
if loss_t >= train_state['early_stopping_best_val']:
#更新早停步数
train_state['early_stopping_step'] += 1
#如果损失减少了
else:
#保存最佳模型
if loss_t < train_state['early_stopping_best_val']:
torch.save(model.state_dict(), train_state['model_filename'])
#重置早停步数
train_state['early_stopping_step'] = 0
#是否提前停止训练?
train_state['stop_early'] = \
train_state['early_stopping_step'] >= args.early_stopping_criteria
return train_state
#将分类器模型移动到指定的设备上
classifier = classifier.to(args.device)
#将数据集的类别权重也移动到相同的设备上
dataset.class_weights = dataset.class_weights.to(args.device)
#定义损失函数为交叉熵损失函数,并使用数据集的类别权重
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
#定义优化器为Adam优化器,用于更新分类器模型的参数,设置学习率为args.learning_rate
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
#定义学习率调度器,当验证损失不再减少时,将学习率按照指定因子0.5进行减少,最小等待轮数为1
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:
#遍历每个epoch
for epoch_index in range(args.num_epochs):
#将当前epoch索引记录到训练状态字典中
train_state['epoch_index'] = epoch_index
#遍历训练数据集
#设置训练模式,准备生成批次数据、将损失和准确率归零
dataset.set_split('train') #设置数据集为训练集
batch_generator = generate_batches(dataset, #创建一个批次生成器,用于获取训练集的数据
batch_size=args.batch_size, #指定批次大小
device=args.device)#指定数据存储设备
running_loss = 0.0 #初始化累计损失为0
running_acc = 0.0 #初始化累计准确率为0
classifier.train() #将分类器模型设置为训练模式,启用dropout等训练相关操作
for batch_index, batch_dict in enumerate(batch_generator):
# the training routine is these 5 steps:
# --------------------------------------
#步骤 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)
#遍历验证集数据集
#设置: 创建验证集批次生成器、将损失和准确率归零、设置评估模式
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()#将分类器模型设置为评估模式,不启用dropout等训练相关操作
for batch_index, batch_dict in enumerate(batch_generator):#遍历批次生成器中的每个批次数据
#计算模型的输出
y_pred = classifier(batch_dict['x_surname'])
#步骤 3. 计算损失
loss = loss_func(y_pred, batch_dict['y_nationality'])#计算损失
loss_t = loss.to("cpu").item()#将损失值移到CPU并提取为Python数值
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])
#如果触发了提前停止条件,结束训练
if train_state['stop_early']:
break
#重置训练和验证进度条的显示
train_bar.n = 0
val_bar.n = 0
epoch_bar.update()#更新epoch进度条显示
except KeyboardInterrupt:
print("Exiting loop")#如果捕获到键盘中断,打印退出循环的消息
2.4.5模型评估与预测
在测试集上进行评估
该模型对测试数据的准确性达到50%左右。如果在附带的notebook中运行训练例程,会注意到在训练数据上的性能更高。这是因为模型总是更适合它所训练的数据,所以训练数据的性能并不代表新数据的性能。如果遵循代码,你可以尝试隐藏维度的不同大小,应该注意到性能的提高。然而,这种增长不会很大。其主要原因是收缩的onehot向量化方法是一种弱表示。虽然它确实简洁地将每个姓氏表示为单个向量,但它丢弃了字符之间的顺序信息,这对于识别起源非常重要。
为新姓氏分类
给定一个姓氏作为字符串,下面的函数首先应用向量化过程,然后获得模型预测,结果包含概率。模型预测,在多项式的情况下,是类概率的列表。我们使用PyTorch张量最大函数来得到由最高预测概率表示的最优类。
def predict_nationality(name, classifier, vectorizer):
#将名字转换为向量表达
vectorized_name = vectorizer.vectorize(name)
#将向量化后的名字转换成 PyTorch 的 Tensor,并进行形状调整为 (1, -1)
vectorized_name = torch.tensor(vectorized_name).view(1, -1)
#使用分类器进行预测,同时应用softmax函数
result = classifier(vectorized_name, 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}
检索一个新姓氏的前K个预测
以下函数实现了输入一个新的姓氏,输出前K个预测,即最有可能是该姓氏所在的前K个国家
def predict_topk_nationality(name, classifier, vectorizer, k=5):
#转换为向量表达
vectorized_name = vectorizer.vectorize(name)
#将向量化后的名字转换成 PyTorch 的 Tensor,并进行形状调整为 (1, -1)
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)
#将概率值和索引从PyTorch Tensor转换为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
2.4.6规则化MLP:权重规则化和结构规则化(dropout)
除了将权值正则化外,对于深度模型名为dropout的结构正则化方法对于避免过拟合也非常重要。
DROPOUT
Dropout可以作为训练深度神经网络的一种trick供选择。在每个训练批次中,通过忽略一半的特征检测器(让一半的隐层节点值为0),可以明显地减少过拟合现象。这种方式可以减少特征检测器(隐层节点)间的相互作用,检测器相互作用是指某些检测器依赖其他检测器才能发挥作用。
换句话说,Dropout也就是在前向传播的时候,让某个神经元的激活值以一定的概率p停止工作,这样可以使模型泛化性更强,因为它不会太依赖某些局部的特征,
通过概率地丢弃单元之间的连接,可以确保没有一个单元总是依赖于另一个单元,从而产生健壮的模型。dropout不会向模型中添加额外的参数,但是需要一个超参数——“drop probability”。drop probability,它是单位之间的连接drop的概率。通常将下降概率设置为0.5。下面代码给出了一个带dropout的MLP的重新实现。
import torch.nn as nn
import torch.nn.functional as F
class MultilayerPerceptron(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
"""初始化多层感知机模型
Args:
input_dim (int): 输入向量的大小
hidden_dim (int): 第一个线性层的输出大小
output_dim (int): 第二个线性层的输出大小
"""
super(MultilayerPerceptron, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim) #第一个全连接层
self.fc2 = nn.Linear(hidden_dim, output_dim) #第二个全连接层
def forward(self, x_in, apply_softmax=False):
"""多层感知机的前向传播
Args:
x_in (torch.Tensor):输入数据张量
x_in.shape should be (batch, input_dim)
apply_softmax (bool): softmax 激活的标志。
如果用于交叉熵损失,应为 False
Returns:
the resulting tensor. tensor.shape should be (batch, output_dim)
"""
intermediate = F.relu(self.fc1(x_in))#第一个全连接层后使用 ReLU 激活函数
output = self.fc2(F.dropout(intermediate, p=0.5))#第二个全连接层后使用0.5的dropout
if apply_softmax:
output = F.softmax(output, dim=1)#如果指定应用softmax,则对输出进行 softmax 操作
return output
但是dropout只适用于训练期间,不适用于评估期间
三、 卷积神经网络
3.1卷积神经网络概念
卷积神经网络(CNN)是一种前馈神经网络,它受生物学上感受野(Receptive Field)的机制而提出的
如上图所示,核是一个小的方阵,它被系统地应用于输入矩阵的不同位置。
在每次应用中,核乘以输入矩阵的值及其自身的值,然后将这些乘法相加kernel具有以下超参数配置:kernel_size=2,stride=1,padding=0,以及dilation=1。这些超参数解释如下:
虽然经典卷积是通过指定核的具体值来设计的,但是CNN是通过指定控制CNN行为的超参数来设计的,然后使用梯度下降来为给定数据集找到最佳参数。两个主要的超参数控制卷积的形状(称为kernel_size)和卷积将在输入数据张量(称为stride)中相乘的位置。还有一些额外的超参数控制输入数据张量被0填充了多少(称为padding),以及当应用到输入数据张量(称为dilation)时,乘法应该相隔多远。
3.1.1卷积运算的维数
卷积运算可以在不同维度上进行,通常用于处理二维(如图像)或一维(如文本序列)数据。在二维卷积中,我们在图像的宽度和高度上进行卷积操作。
3.1.2卷积运算的通道数
图像或特征图中的通道数指的是在每个空间位置上同时存在的特征映射数量。例如,彩色图像有RGB三个通道,每个通道分别代表红色、绿色和蓝色通道。
3.1.3卷积核大小
卷积核是卷积操作中用于提取特征的小矩阵。KERNEL大小指的是卷积核的宽度和高度,通常以像素为单位。卷积核的大小影响了卷积操作提取特征的范围和粒度。
3.1.4步幅
步幅是卷积核在输入数据上滑动的步长。步幅决定了卷积核每次移动的像素数目。较大的步幅可以减少输出特征图的尺寸,而较小的步幅可以保持更多的空间信息。
3.1.5PADDING(填充)
填充是在输入数据周围添加额外的值(通常是0),以便在卷积操作时保持输入和输出的尺寸。填充可以帮助有效地处理边缘像素和保持输出特征图与输入的大小一致。
3.1.6膨胀卷积
膨胀卷积(或称为空洞卷积)是一种卷积操作,其中卷积核具有间隔地分布的零元素,这些零元素允许卷积核在输入数据上进行更大范围的感受野采样,而不增加参数量和计算成本。
3.2在PyTorch中实现CNNs
一般来说,神经网络设计的目标是找到一个能够完成任务的超参数组态。我们仍然需要应用最后一个线性层,它将学会从一系列卷积层创建的特征向量创建预测向量。这意味着目标是确定卷积层的配置,从而得到所需的特征向量。所有CNN应用程序都是这样的:首先有一组卷积层,它们提取一个feature map,然后将其作为上游处理的输入。在分类中,上游处理几乎总是应用线性(或fc)层。
我们首先构造一个人工数据张量,以反映实际数据的形状。数据张量的大小是三维的——这是向量化文本数据的最小批大小。如果你对一个字符序列中的每个字符使用onehot向量,那么onehot向量序列就是一个矩阵,而onehot矩阵的小批量就是一个三维张量。使用卷积的术语,每个onehot(通常是词汇表的大小)的大小是”input channels”的数量,字符序列的长度是“width”。
3.3使用CNN对姓氏进行分类
在下面的例子中将使用一个简单的CNN模型来分类姓氏。在前期的一些准备工作中,大部分细节与上面,发生变化的是模型的构造和向量化过程。模型的输入改为一个onehot的矩阵。这种设计将使CNN能够更好地“view”字符的排列,并对在“多层感知器的姓氏分类”中使用的收缩的onehot编码中丢失的序列信息进行编码。
3.3.1姓氏数据集
在下面实现了一个数据集类,它跟踪最长的姓氏,并将其作为矩阵中包含的行数提供给矢量化器。列的数量是onehot向量的大小(词汇表的大小)。下述代码显示了对SurnameDataset.__getitem__
的更改;我们显示对SurnameVectorizer的更改。在下一小节向量化。
我们使用数据集中最长的姓氏来控制onehot矩阵的大小有两个原因。首先,将每一小批姓氏矩阵组合成一个三维张量,要求它们的大小相同。其次,使用数据集中最长的姓氏意味着可以以相同的方式处理每个小批处理。
class SurnameDataset(Dataset):
def __init__(self, surname_df, vectorizer):
"""
Args:
name_df (pandas.DataFrame): 数据集
vectorizer (SurnameVectorizer): 从数据集实例化的向量化器
"""
self.surname_df = surname_df
self._vectorizer = vectorizer
#划分数据集为训练集、验证集和测试集
self.train_df = self.surname_df[self.surname_df.split=='train']
self.train_size = len(self.train_df)
self.val_df = self.surname_df[self.surname_df.split=='val']
self.validation_size = len(self.val_df)
self.test_df = self.surname_df[self.surname_df.split=='test']
self.test_size = len(self.test_df)
#为每个数据集创建查找字典,方便后续获取
self._lookup_dict = {'train': (self.train_df, self.train_size),
'val': (self.val_df, self.validation_size),
'test': (self.test_df, self.test_size)}
#设置默认使用训练集
self.set_split('train')
#类别权重
class_counts = surname_df.nationality.value_counts().to_dict()
#按照国籍词汇表的顺序对类别进行排序,以确保一致性
def sort_key(item):
return self._vectorizer.nationality_vocab.lookup_token(item[0])
sorted_counts = sorted(class_counts.items(), key=sort_key)
frequencies = [count for _, count in sorted_counts]
self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)
@classmethod
def load_dataset_and_make_vectorizer(cls, surname_csv):
"""加载数据集并从头开始创建一个新的向量化器
Args:
surname_csv (str): 数据集的位置
Returns:
SurnameDataset的一个实例
"""
surname_df = pd.read_csv(surname_csv)#读取数据
train_surname_df = surname_df[surname_df.split=='train']
#从训练集数据框创建SurnameVectorizer
return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
@classmethod
def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
"""Load dataset and the corresponding vectorizer.
Used in the case in the vectorizer has been cached for re-use
Args:
surname_csv (str):姓氏数据的位置
vectorizer_filepath (str): 保存的 vectorizer的位置
Returns:
一个SurnameDataset的实例
"""
surname_df = pd.read_csv(surname_csv)#从CSV文件中读取姓氏数据集
vectorizer = cls.load_vectorizer_only(vectorizer_filepath)#调用静态方法加载向量化器
return cls(surname_df, vectorizer)
@staticmethod
def load_vectorizer_only(vectorizer_filepath):
"""
Args:
vectorizer_filepath (str): 序列化的向量化器文件的位置
Returns:
一个SurnameDataset实例
"""
with open(vectorizer_filepath) as fp:#打开向量化器文件
return SurnameVectorizer.from_serializable(json.load(fp))#使用JSON加载并反序列化向量化器
def save_vectorizer(self, vectorizer_filepath):
"""saves the vectorizer to disk using json
Args:
vectorizer_filepath (str): # 要保存向量化器的文件位置
"""
with open(vectorizer_filepath, "w") as fp:#打开文件以写入模式
json.dump(self._vectorizer.to_serializable(), fp)#将向量化器序列化为JSON格式并写入文件
def get_vectorizer(self):
""" returns the vectorizer """# 返回当前实例中的向量化器
return self._vectorizer
def set_split(self, split="train"):
""" selects the splits in the dataset using a column in the dataframe """
self._target_split = split#设置目标数据集拆分为指定的拆分名称
self._target_df, self._target_size = self._lookup_dict[split]#根据拆分名称更新目标数据集和数据集大小
def __len__(self):
return self._target_size#返回数据集的大小
def __getitem__(self, index):
"""PyTorch数据集的主要入口
Args:
index (int): 数据点的索引
Returns:
a dictionary holding the data point's features (x_data) and label (y_target)
"""
row = self._target_df.iloc[index]#获取目标数据集中指定索引位置的行数据
#使用向量化器对姓氏进行向量化
surname_matrix = \
self._vectorizer.vectorize(row.surname)
#查找姓氏对应的国籍索引
nationality_index = \
self._vectorizer.nationality_vocab.lookup_token(row.nationality)
#返回包含向量化的姓氏和国籍索引的字典
return {'x_surname': surname_matrix,
'y_nationality': nationality_index}
def get_num_batches(self, batch_size):
"""给定批次大小,返回数据集中的批次数
Args:
批次大小
Returns:
数据集中的批次数
"""
return len(self) // batch_size#计算数据集中批次的数量
def generate_batches(dataset, batch_size, shuffle=True,
drop_last=True, device="cpu"):
"""
生成器函数,包装了 PyTorch DataLoader,确保每个张量在正确的设备上。
"""
dataloader = DataLoader(dataset=dataset, batch_size=batch_size,
shuffle=shuffle, drop_last=drop_last)#创建DataLoader对象
for data_dict in dataloader:
out_data_dict = {}
for name, tensor in data_dict.items():
out_data_dict[name] = data_dict[name].to(device)#将数据移到指定的设备上
yield out_data_dict#返回包含批次数据的字典
3.3.2词汇表,向量化器, DataLoader
词汇表和DataLoader的实现方式与“多层感知器的姓氏分类”中的相同
向量化器的vectorize()方法要进行修改,以适应CNN模型的需要。具体来说,该函数将字符串中的每个字符映射到一个整数,然后使用该整数构造一个由onehot向量组成的矩阵,矩阵中的每一列都是不同的onehot向量。
除了更改为使用onehot矩阵之外,我们还修改了矢量化器,以便计算姓氏的最大长度并将其保存为max_surname_length
class SurnameVectorizer(object):
"""协调词汇表并将其用于向量化的向量化器"""
def __init__(self, surname_vocab, nationality_vocab, max_surname_length):
"""
Args:
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):
"""
Args:
surname (str): 姓氏
Returns:
one_hot_matrix (np.ndarray): 一个独热向量的矩阵
"""
#初始化一个全零矩阵,大小为 (字符词汇表的长度, 最大姓氏长度)
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)
#将对应位置设置为 1,表示独热向量
one_hot_matrix[character_index][position_index] = 1
return one_hot_matrix
@classmethod
def from_dataframe(cls, surname_df):
"""从数据集的 DataFrame 实例化向量化器
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)
#返回SurnameVectorizer的实例
return cls(character_vocab, nationality_vocab, max_surname_length)
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.3用卷积网络重新实现姓氏分类器
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(),#使用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 should be (batch, initial_num_channels,
max_surname_length)
apply_softmax (bool): softmax 激活的标志,
如果用于交叉熵损失,则应为 False
Returns:
the resulting tensor. tensor.shape should be (batch, num_classes)
"""
#使用定义好的卷积神经网络进行特征提
features = self.convnet(x_surname).squeeze(dim=2)
#使用全连接层进行分类预测
prediction_vector = self.fc(features)
#如果需要应用softmax激活函数
if apply_softmax:
prediction_vector = F.softmax(prediction_vector, dim=1)
return prediction_vector
训练程序:实例化数据集,实例化模型,实例化损失函数,实例化优化器,遍历数据集的训练分区和更新模型参数,遍历数据集的验证分区和测量性能,然后重复数据集迭代一定次数。由于与上述多层感知机完成姓氏分类的过程类似,只有个别参数不一样,故不再赘述。