前言
前馈神经网络(Feedforward Neural Network,简称FFNN)是一种基本的人工神经网络模型,也称多层感知机,是深度学习的基础之一。
它的结构由输入层、若干个隐藏层和输出层组成,层与层之间的节点是全连接的,即每个节点与上一层的所有节点相连,而且每个连接都有一个权重,表示其输入变量的重要程度。
本实验讲基于环境Python 3.6.7,深入探究多层感知器在多层分类中的应用以及每种类型的神经网络层对它所计算的数据张量的大小和形状的影响
一、网络结构基础
多层感知器是一种经典的前馈神经网络模型,具有多个隐藏层(即中间层)的结构。它是一种通用的非线性函数逼近器,能够在许多机器学习任务中表现出色,例如分类、回归和特征学习。
下图为一种具有两个线性层和三个表示阶段(输入向量、隐藏向量和输出向量)的MLP的可视化表示
1.1输入层(Input Vector):
输入层是神经网络的第一层,负责接收原始数据或特征向量作为输入。
-
数据传递:输入层将原始数据或预处理后的特征向量传递给神经网络的下一层。
-
节点:每个输入特征对应输入层的一个节点。例如,对于图像数据,每个像素点或提取的特征可以作为一个节点的输入。
-
数据编码:输入层负责将不同类型的数据(如数字、文本、图像等)适当地编码为网络可以理解的格式,通常是数值向量。
1.2隐藏层(Hidden Vector):
在输入层和输出层之间的层称为隐藏层。每个隐藏层由多个神经元(节点)组成,每个神经元接收来自前一层所有节点的输入,并应用激活函数后输出到下一层。
-
特征提取:隐藏层通过一系列权重和偏置的线性组合,结合非线性的激活函数,将输入特征转换为更高级别的表征或特征。
-
层与层之间的连接:每个隐藏层的节点与前一层(可能是输入层或上一个隐藏层)的所有节点相连,形成全连接结构或者部分连接结构(如卷积神经网络中的局部连接)。
-
深度表征学习:多个隐藏层的堆叠使得神经网络能够学习到更加复杂和抽象的特征表示,这种结构使得神经网络能够有效地处理复杂的输入-输出关系。
1.3输出层(Output Vector):
提供最终的预测或分类结果。输出层的节点数取决于问题的类型:对于分类问题,每个类别可能对应一个输出节点;对于回归问题,通常只有一个输出节点。
-
结果输出:输出层产生神经网络对于特定输入的最终预测或分类结果。
-
节点:输出层的节点数通常与问题的输出类别或预测目标相关联。例如,对于二分类问题,可能有一个输出节点表示两个类别中的一个;对于多分类问题,每个类别可能有一个输出节点;对于回归问题,通常只有一个输出节点。
-
激活函数:输出层的每个节点通常使用适当的激活函数,如sigmoid函数(对于二分类问题)、softmax函数(对于多分类问题)、线性函数(对于回归问题)等,以将神经网络的输出映射到适当的范围或概率分布。
二、用多层感知机对姓氏进行分类
首先导入一些需要用到的Python库和模块,提供从命令行参数解析、数据处理、深度学习模型构建到训练优化等各个方面所需的基础功能和工具。
from argparse import Namespace
from collections import Counter
import json
import os
import string
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm_notebook
2.1词汇
定义一个名为 Vocabulary
的类,用于管理文本数据的词汇表,包括添加新的标记、获取标记索引、序列化词汇表等。它适合用于自然语言处理任务中,如将文本转换为模型可接受的数字表示形式(索引化)。
class Vocabulary(object):
"""用于处理文本并提取词汇表以进行映射的类"""
def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
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:
# 如果指定了添加UNK标记,则将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):
"""从一个序列化的字典中实例化词汇表"""
return cls(**contents)
def add_token(self, 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):
return [self.add_token(token) for token in tokens]
def lookup_token(self, token):
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):
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)
2.2矢量化器
将姓氏(surname)转换为 one-hot 编码的向量,基于提供的姓氏词汇表(surname_vocab
)。
如假设有一个 surname_vocab
包含字符到整数的映射,比如 {'A': 0, 'B': 1, ..., 'Z': 25},对于输入的姓氏 "Smith",该类会生成一个长度为26的向量,其中 'S', 'm', 'i', 't', 'h' 对应的位置被设为1,其余位置为0。
这种方式可以有效地将文本数据转换为机器学习算法可以处理的数值表示形式
class SurnameVectorizer(object):
"""协调词汇表并将它们用于向量化的向量化器"""
def __init__(self, surname_vocab, nationality_vocab):
self.surname_vocab = surname_vocab
self.nationality_vocab = nationality_vocab
def vectorize(self, surname):
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_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()}
比如:可以通过传入一个包含姓氏数据和国籍的 DataFrame 来创建一个 SurnameVectorizer
,它会自动构建姓氏和国籍的词汇表。
import pandas as pd
surname_data = {'surname': ['Smith', 'Johnson', 'Williams'], 'nationality': ['English', 'English', 'English']}
df = pd.DataFrame(surname_data)
vectorizer = SurnameVectorizer.from_dataframe(df)
将 SurnameVectorizer
对象序列化为字典,然后再从字典反序列化为对象。
# Serialize
vectorizer_dict = vectorizer.to_serializable()
# Deserialize
restored_vectorizer = SurnameVectorizer.from_serializable(vectorizer_dict)
这样的设计使得 SurnameVectorizer
类具有灵活性和可重用性,能够方便地从不同数据源创建实例。
2.3数据集
处理完成后,我们首先自定义的数据集类,初始化姓氏数据集对象,并准备好不同数据集划分的数据框、大小以及类别权重,以便后续在机器学习或深度学习模型中使用。
SurnameDataset主要
用于处理姓氏数据集。
接着划分数据集:
train_df
,val_df
,test_df
: 分别表示训练集、验证集和测试集的数据框train_size
,validation_size
,test_size
: 分别表示训练集、验证集和测试集的大小(数据框中的行数)
class SurnameDataset(Dataset):
def __init__(self, surname_df, vectorizer):
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 weights
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)
其中class_weights
: 是一个张量,包含每个类别的权重,用于在训练模型时处理类别不平衡问题。权重计算基于每个国籍出现的频率,并且频率越高,权重越低。
紧接着我们进行数据集的处理。包括加载数据集、创建和保存向量化器、定义数据集类来处理数据加载和转换,以及一个生成批次的函数来实现批量数据的迭代和设备移动。
@classmethod
def load_dataset_and_make_vectorizer(cls, surname_csv):
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 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):
with open(vectorizer_filepath) as fp:
return SurnameVectorizer.from_serializable(json.load(fp))
def save_vectorizer(self, vectorizer_filepath):
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):
row = self._target_df.iloc[index]
surname_vector = \
self._vectorizer.vectorize(row.surname)
nationality_index = \
self._vectorizer.nationality_vocab.lookup_token(row.nationality)
return {'x_surname': surname_vector,
'y_nationality': nationality_index}
def get_num_batches(self, batch_size):
return len(self) // batch_size
def generate_batches(dataset, batch_size, shuffle=True,
drop_last=True, device="cpu"):
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模型:SurnameClassifier
下面我们进行分类姓氏
self.fc1
和 self.fc2
分别是两个线性层(全连接层):
self.fc1
将输入向量的维度映射到隐藏层的维度hidden_dim
。self.fc2
将隐藏层的维度hidden_dim
映射到输出的维度output_dim
。.
SurnameClassifier
类实现了一个简单的两层多层感知机(MLP),用于对输入的姓氏数据进行分类,其前向传播方法定义了数据在模型中的流动方式,并最终产生预测结果。
class SurnameClassifier(nn.Module):
"""一个用于分类姓氏的两层多层感知机"""
def __init__(self, input_dim, hidden_dim, output_dim):
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):
intermediate_vector = F.relu(self.fc1(x_in))
prediction_vector = self.fc2(intermediate_vector)
if apply_softmax:
prediction_vector = F.softmax(prediction_vector, dim=1)
return prediction_vector
2.5训练过程
在训练开始前,创建一个结构化的状态容器train_state
,用于跟踪和更新训练过程中的各种指标和设置。具体实施步骤:
定义一个函数 make_train_state
,它接受一个参数 args
,假设 args
是一个包含了训练配置信息的对象或者命名空间。然后返回一个字典,包含了训练过程中的状态信息。
def make_train_state(args):
return {'stop_early': False,
'early_stopping_step': 0,
'early_stopping_best_val': 1e8,
'learning_rate': args.learning_rate,
'epoch_index': 0,
'train_loss': [],
'train_acc': [],
'val_loss': [],
'val_acc': [],
'test_loss': -1,
'test_acc': -1,
'model_filename': args.model_state_file}
更新训练状态 train_state
,并根据一些条件决定是否保存模型或提前停止训练。具体步骤如下:
-
保存第一个模型:
- 如果
train_state['epoch_index']
等于 0,表示当前是第一个 epoch。 - 在第一个 epoch 时,将模型的状态字典
model.state_dict()
保存到指定的文件train_state['model_filename']
。 - 将
train_state['stop_early']
设置为False
,表示不提前停止训练。
- 如果
-
检查模型性能是否提升:
- 如果
train_state['epoch_index']
大于等于 1,表示不是第一个 epoch。 - 从
train_state['val_loss']
列表中获取倒数第二个和最后一个元素,分别赋值给loss_tm1
和loss_t
。 - 如果
loss_t
大于等于train_state['early_stopping_best_val']
,说明验证集损失增加,此时:更新train_state['early_stopping_step']
表示连续验证集损失没有改善的步数。 - 如果
loss_t
小于train_state['early_stopping_best_val']
,说明验证集损失减少:- 使用
torch.save()
保存当前模型状态字典到train_state['model_filename']
。 - 将
train_state['early_stopping_step']
重置为 0,重新计数。
- 使用
- 如果
-
提前停止训练判断:
- 检查是否达到提前停止的条件,即
train_state['early_stopping_step']
是否大于等于args.early_stopping_criteria
。 - 如果达到条件,则将
train_state['stop_early']
设置为True
,指示需要提前停止训练。
- 检查是否达到提前停止的条件,即
-
返回更新后的
train_state
。
def update_train_state(args, model, train_state):
# 至少保存一个模型
if train_state['epoch_index'] == 0:
# 如果是第一个 epoch,则保存模型
torch.save(model.state_dict(), train_state['model_filename'])
# 将提前停止标志置为 False
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
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 set_seed_everywhere(seed, cuda):
np.random.seed(seed)
torch.manual_seed(seed)
if cuda:
torch.cuda.manual_seed_all(seed)
def handle_dirs(dirpath):
if not os.path.exists(dirpath):
os.makedirs(dirpath)
进行参数配置、路径管理、CUDA设备选择、随机种子设置以及目录创建等任务,为训练神经网络模型提供基本的环境和配置。
-
Namespace定义和初始化
-
路径扩展和打印:
- 如果
expand_filepaths_to_save_dir
为True
,则将vectorizer_file
和model_state_file
的路径扩展到save_dir
目录下。 - 打印扩展后的文件路径,用于确认文件保存位置。
3.CUDA检查和设备设置:
- 检查当前环境是否支持 CUDA,如果不支持则将
args.cuda
设置为False
。 - 根据
args.cuda
的值选择使用 CUDA 还是 CPU 运行模型。
4.设置随机种子:
- 调用
set_seed_everywhere(args.seed, args.cuda)
函数,设置随机种子以确保实验结果的可重复性。
5.处理目录:
- 调用
handle_dirs(args.save_dir)
函数,确保保存模型的目录存在,如果不存在则创建该目录。
args = Namespace(
# Data and path information
surname_csv="data/surnames/surnames_with_splits.csv",
vectorizer_file="vectorizer.json",
model_state_file="model.pth",
save_dir="model_storage/ch4/surname_mlp",
# Model hyper parameters
hidden_dim=300,
# Training hyper parameters
seed=1337,
num_epochs=100,
early_stopping_criteria=5,
learning_rate=0.001,
batch_size=64,
# Runtime options
cuda=False,
reload_from_files=False,
expand_filepaths_to_save_dir=True,
)
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))
# Check CUDA
if not torch.cuda.is_available():
args.cuda = False
args.device = torch.device("cuda" if args.cuda else "cpu")
print("Using CUDA: {}".format(args.cuda))
# Set seed for reproducibility
set_seed_everywhere(args.seed, args.cuda)
# handle dirs
handle_dirs(args.save_dir)
结果输出
初始化
判断是否从文件中重新加载
- 如果
args.reload_from_files
为True
,表示要从之前的检查点重新加载数据和向量化器。 - 如果不重新加载,创建新的数据集和向量化器:
接着基于向量化器的词汇表大小创建用于姓氏分类的神经网络模型。
if args.reload_from_files:
# training from a checkpoint
print("Reloading!")
dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
args.vectorizer_file)
else:
# create dataset and vectorizer
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))
训练循环
首先进行神经网络的训练设置各种必要的组件和工具,包括设备分配、损失函数、优化器、学习率调度器以及用于跟踪训练状态和显示进度的进度条。
定义损失函数、优化器和学习率调度器:
loss_func
: 使用交叉熵损失函数 (nn.CrossEntropyLoss
),并将数据集的类别权重传递给它,以便在训练期间考虑类别不平衡问题。optimizer
: 使用 Adam 优化器,用于调整神经网络模型classifier
的参数,学习率为args.learning_rate
。scheduler
: 使用学习率调度器 (optim.lr_scheduler.ReduceLROnPlateau
),当验证集上的性能不再提升时,以指数方式减少学习率,有助于稳定和优化训练过程。
classifier = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)
loss_func = nn.CrossEntropyLoss(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)
准备训练循环的起始部分:
展示了如何训练一个姓氏分类器模型,并在每个训练周期结束后使用验证集评估模型的表现。
设置了数据集为训练模式,创建了用于生成批次数据的生成器,并初始化了损失和准确率的累积变量。同时,将神经网络模型设置为训练模式,以便在训练过程中进行参数更新和梯度计算。
这个循环会持续迭代每个批次直到当前epoch的所有数据都被处理完毕。通过这种方式,模型在每个epoch中通过多个步骤(计算损失、反向传播、优化)逐渐调整自己的参数,以最小化损失函数,并在训练过程中评估准确率。
try:
for epoch_index in range(args.num_epochs):
train_state['epoch_index'] = epoch_index
# Iterate over training dataset
# setup: batch generator, set loss and acc to 0, set train mode on
dataset.set_split('train')
batch_generator = generate_batches(dataset,
batch_size=args.batch_size,
device=args.device)
running_loss = 0.0
running_acc = 0.0
classifier.train()
for batch_index, batch_dict in enumerate(batch_generator):
# the training routine is these 5 steps:
# --------------------------------------
# step 1. zero the gradients
optimizer.zero_grad()
# step 2. compute the output
y_pred = classifier(batch_dict['x_surname'])
# step 3. compute the loss
loss = loss_func(y_pred, batch_dict['y_nationality'])
loss_t = loss.item()
running_loss += (loss_t - running_loss) / (batch_index + 1)
# step 4. use loss to produce gradients
loss.backward()
# step 5. use optimizer to take gradient step
optimizer.step()
# -----------------------------------------
# compute the accuracy
acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
running_acc += (acc_t - running_acc) / (batch_index + 1)
# update bar
train_bar.set_postfix(loss=running_loss, acc=running_acc,
epoch=epoch_index)
train_bar.update()
train_state['train_loss'].append(running_loss)
train_state['train_acc'].append(running_acc)
# Iterate over val dataset
# setup: batch generator, set loss and acc to 0; set eval mode on
dataset.set_split('val')
batch_generator = generate_batches(dataset,
batch_size=args.batch_size,
device=args.device)
running_loss = 0.
running_acc = 0.
classifier.eval()
for batch_index, batch_dict in enumerate(batch_generator):
# compute the output
y_pred = classifier(batch_dict['x_surname'])
# step 3. compute the loss
loss = loss_func(y_pred, batch_dict['y_nationality'])
loss_t = loss.to("cpu").item()
running_loss += (loss_t - running_loss) / (batch_index + 1)
# compute the accuracy
acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
running_acc += (acc_t - running_acc) / (batch_index + 1)
val_bar.set_postfix(loss=running_loss, acc=running_acc,
epoch=epoch_index)
val_bar.update()
train_state['val_loss'].append(running_loss)
train_state['val_acc'].append(running_acc)
train_state = update_train_state(args=args, model=classifier,
train_state=train_state)
scheduler.step(train_state['val_loss'][-1])
if train_state['stop_early']:
break
train_bar.n = 0
val_bar.n = 0
epoch_bar.update()
except KeyboardInterrupt:
print("Exiting loop")
评估经过训练和调优的模型在测试集上的表现,通过计算测试集的损失和准确率来衡量模型的泛化能力和性能。
# compute the loss & accuracy on the test set using the best available model
classifier.load_state_dict(torch.load(train_state['model_filename']))
classifier = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
dataset.set_split('test')
batch_generator = generate_batches(dataset,
batch_size=args.batch_size,
device=args.device)
running_loss = 0.
running_acc = 0.
classifier.eval()
for batch_index, batch_dict in enumerate(batch_generator):
# compute the output
y_pred = classifier(batch_dict['x_surname'])
# compute the loss
loss = loss_func(y_pred, batch_dict['y_nationality'])
loss_t = loss.item()
running_loss += (loss_t - running_loss) / (batch_index + 1)
# compute the accuracy
acc_t = compute_accuracy(y_pred, batch_dict['y_nationality'])
running_acc += (acc_t - running_acc) / (batch_index + 1)
train_state['test_loss'] = running_loss
train_state['test_acc'] = running_acc
打印测试集上的损失和准确率。在打印时,我使用了格式化字符串来确保损失值显示为小数点后四位,准确率显示为小数点后两位的百分数形式。
train_state['test_loss']
包含了测试集上的平均损失值。train_state['test_acc']
包含了测试集上的平均准确率值,通常是一个在0到1之间的小数。
print("Test loss: {:.4f}".format(train_state['test_loss']))
print("Test Accuracy: {:.2f}%".format(train_state['test_acc'] * 100))
定义一个函数 predict_nationality
,用于预测一个姓氏对应的国籍
这种设计使得函数能够接受一个姓氏作为输入,并使用训练好的分类器来预测其可能的国籍,并返回预测的国籍及其对应的概率值。
def predict_nationality(surname, classifier, vectorizer):
"""Predict the nationality from a new surname
Args:
surname (str): the surname to classifier
classifier (SurnameClassifer): an instance of the classifier
vectorizer (SurnameVectorizer): the corresponding vectorizer
Returns:
a dictionary with the most likely nationality and its probability
"""
vectorized_surname = vectorizer.vectorize(surname)
vectorized_surname = torch.tensor(vectorized_surname).view(1, -1)
result = classifier(vectorized_surname, apply_softmax=True)
probability_values, indices = result.max(dim=1)
index = indices.item()
predicted_nationality = vectorizer.nationality_vocab.lookup_index(index)
probability_value = probability_values.item()
return {'nationality': predicted_nationality, 'probability': probability_value}
从用户输入中获取一个姓氏,将姓氏输入给模型,然后使用预先训练好的模型来预测该姓氏对应的国籍,得到预测结果和对应的概率值,并将预测结果格式化后打印出来,让用户能够看到模型的预测信息。
new_surname = input("Enter a surname to classify: ")
classifier = classifier.to("cpu")
prediction = predict_nationality(new_surname, classifier, vectorizer)
print("{} -> {} (p={:0.2f})".format(new_surname,
prediction['nationality'],
prediction['probability']))
这段代码片段中的 vectorizer.nationality_vocab.lookup_index(8)
是在 vectorizer
对象中查找索引为 8 的国籍。
通常情况下,vectorizer
对象在文本预处理过程中用于将文本数据(如姓氏)转换成机器学习模型可以处理的向量形式。nationality_vocab
是一个词汇表,用于将国籍(或其他类别)映射到整数索引,以便于模型训练和预测。
因此,lookup_index(8)
的作用是返回在词汇表中索引为 8 的国籍。具体来说,它会返回词汇表中索引为 8 的国籍的文本表示或者名称。
vectorizer.nationality_vocab.lookup_index(8)
提供交互式界面,使用一个预训练的分类器模型,对用户输入的姓氏进行国籍预测,并输出预测的前 k 个国籍及其对应的概率值,以及预测的相对可信程度。
def predict_topk_nationality(name, classifier, vectorizer, k=5):
vectorized_name = vectorizer.vectorize(name)
vectorized_name = torch.tensor(vectorized_name).view(1, -1)
prediction_vector = classifier(vectorized_name, apply_softmax=True)
probability_values, indices = torch.topk(prediction_vector, k=k)
# returned size is 1,k
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: ")
classifier = classifier.to("cpu")
k = int(input("How many of the top predictions to see? "))
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)
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']))