基于多层感知器(MLP)的姓氏分类

一. MLP简介

       

1. 什么是MLP?

多层感知机(MLP)是一种前馈神经网络,由至少三层神经元组成:输入层、隐藏层(可能有多层),以及输出层。每一层都与下一层全连接,每个连接都有一个权重。MLP是一种通用函数逼近器,它可以学习并表示复杂的非线性关系。

2. MLP结构

  1. 输入层(Input Layer):接受输入特征的层,每个神经元对应一个特征。
  2. 隐藏层(Hidden Layer):在输入层和输出层之间的层,对输入进行加权和求和后应用激活函数产生输出。
  3. 输出层(Output Layer):产生网络输出的层,可以是一个或多个神经元,取决于问题的类型。

其结构可如下图所示: 

                                        

二. 实验介绍

        本次实验旨在利用MLP(多层感知机)进行姓氏分类。MLP是一种前馈神经网络,由输入层、隐藏层和输出层组成。在这个实验中,我们将使用一个包含姓氏及其对应国家或地区的数据集。首先,我们将对姓氏进行预处理,将其转换为数字向量表示,并对数据进行归一化处理。然后,我们将构建一个MLP模型,其中输入层接受数字化的姓氏特征,隐藏层通过加权求和和激活函数处理输入,最终输出层给出对应国家或地区的分类结果,我们将使用训练集对模型进行训练,通过优化算法不断调整模型参数,使其能够更好地拟合训练数据,我们将使用测试集评估训练好的模型在未见过的数据上的性能。通过这个实验,我们希望探索MLP在姓氏分类问题上的应用,以及了解不同文化和地理区域之间的姓氏特征与差异。

三. 实验步骤

1. 数据集Surname Dataset

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

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

class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        初始化函数
        
        参数:
            surname_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):
        """加载数据集并从头开始创建一个新的矢量化器
        
        参数:
            surname_csv (str): 数据集的位置
        返回:
            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):
        """加载数据集和对应的矢量化器。在矢量化器已缓存以供重用的情况下使用
        
        参数:
            surname_csv (str): 数据集的位置
            vectorizer_filepath (str): 保存的矢量化器的位置
        返回:
            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):
        """从文件加载矢量化器的静态方法
        
        参数:
            vectorizer_filepath (str): 序列化矢量化器的位置
        返回:
            SurnameVectorizer的一个实例
        """
        with open(vectorizer_filepath) as fp:
            return SurnameVectorizer.from_serializable(json.load(fp))

    def save_vectorizer(self, vectorizer_filepath):
        """使用json将矢量化器保存到磁盘
        
        参数:
            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数据集的主要入口方法
        
        参数:
            index (int): 数据点的索引 
        返回:
            一个字典,包含数据点的:
                特征 (x_surname)
                标签 (y_nationality)
        """
        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):
        """给定批量大小,返回数据集中的批次数量
        
        参数:
            batch_size (int)
        返回:
            数据集中的批次数量
        """
        return len(self) // batch_size

def generate_batches(dataset, batch_size, shuffle=True, drop_last=True, device="cpu"): 
    """
    一个生成器函数,封装了PyTorch的DataLoader。它将确保每个张量都在正确的设备位置。
    
    参数:
        dataset (Dataset): 数据集
        batch_size (int): 每批的大小
        shuffle (bool): 是否打乱数据
        drop_last (bool): 是否丢弃最后一个不完整的批次
        device (str): 设备类型
    """
    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. 处理文本并提取词汇表方法Vocabulary

       本例中使用的词汇类与“example: Classifying Sentiment of Restaurant Reviews”中的词汇完全相同,该词汇类将Yelp评论中的单词映射到对应的整数。简要概述一下,词汇表是两个Python字典的协调,这两个字典在令牌(在本例中是字符)和整数之间形成一个双射;也就是说,第一个字典将字符映射到整数索引,第二个字典将整数索引映射到字符。add_token方法用于向词汇表中添加新的令牌,lookup_token方法用于检索索引,lookup_index方法用于检索给定索引的令牌(在推断阶段很有用)。与Yelp评论的词汇表不同,我们使用的是one-hot词汇表,不计算字符出现的频率,只对频繁出现的条目进行限制。这主要是因为数据集很小,而且大多数字符足够频繁。以下为该方法的代码:

class Vocabulary(object):
    """处理文本并提取词汇表以进行映射的类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        初始化函数
        
        参数:
            token_to_idx (dict): 预先存在的token到索引的映射字典
            add_unk (bool): 是否添加UNK(未知)token的标志
            unk_token (str): 要添加到词汇表中的UNK token
        """

        # 如果没有提供token_to_idx,则初始化为空字典
        if token_to_idx is None:
            token_to_idx = {}
        self._token_to_idx = token_to_idx

        # 创建从索引到token的反向映射
        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更新映射字典

        参数:
            token (str): 要添加到词汇表中的项目
        返回:
            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添加到词汇表中
        
        参数:
            tokens (list): 字符串token的列表
        返回:
            indices (list): 对应于这些token的索引列表
        """
        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        """检索与token相关联的索引,如果token不存在则返回UNK索引
        
        参数:
            token (str): 要查找的token
        返回:
            index (int): 对应于token的索引
        注意:
            `unk_index`需要>=0(已添加到词汇表中)才能使用UNK功能 
        """
        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):
        """返回与索引相关联的token
        
        参数: 
            index (int): 要查找的索引
        返回:
            token (str): 对应于索引的token
        抛出:
            KeyError: 如果索引不在词汇表中
        """
        if index not in self._idx_to_token:
            raise KeyError("索引 (%d) 不在词汇表中" % 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. 协调词汇表并将其应用的方法SurnameVectorizer

       虽然词汇表将单个令牌(字符)转换为整数,但SurnameVectorizer负责应用词汇表并将姓氏转换为向量。实例化和使用非常类似于“示例:对餐馆评论的情绪进行分类”中的ReviewVectorizer,但有一个关键区别:字符串没有在空格上分割。姓氏是字符的序列,每个字符在我们的词汇表中是一个单独的标记。然而,在“卷积神经网络”出现之前,我们将忽略序列信息,通过迭代字符串输入中的每个字符来创建输入的收缩one-hot向量表示。我们为以前未遇到的字符指定一个特殊的令牌,即UNK。由于我们仅从训练数据实例化词汇表,而且验证或测试数据中可能有惟一的字符,所以在字符词汇表中仍然使用UNK符号。

       虽然我们在这个示例中使用了收缩的one-hot,但是在后面的实验中,将了解其他向量化方法,它们是one-hot编码的替代方法,有时甚至更好。具体来说,在“示例:使用CNN对姓氏进行分类”中,将看到一个热门矩阵,其中每个字符都是矩阵中的一个位置,并具有自己的热门向量。然后,在实验5中,将学习嵌入层,返回整数向量的向量化,以及如何使用它们创建密集向量矩阵。以下为SurnameVectorizer类的代码:

class SurnameVectorizer(object):
    """ 协调词汇表并将其应用的矢量化器类 """
    
    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):
        """
        将姓氏转换为一维one-hot编码向量
        
        参数:
            surname (str): 姓氏

        返回:
            one_hot (np.ndarray): 折叠后的one-hot编码
        """
        vocab = self.surname_vocab
        # 创建一个与词汇表长度相同的零数组
        one_hot = np.zeros(len(vocab), dtype=np.float32)
        # 对姓氏中的每个字符进行one-hot编码
        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()}

4. 姓氏分类的多层感知器SurnameClassifier

       第一个线性层将输入向量映射到中间向量,并对该向量应用非线性。第二线性层将中间向量映射到预测向量。

       在最后一步中,可选地应用softmax操作,以确保输出和为1;这就是所谓的“概率”。它是可选的原因与我们使用的损失函数的数学公式有关——交叉熵损失。我们研究了“损失函数”中的交叉熵损失。回想一下,交叉熵损失对于多类分类是最理想的,但是在训练过程中软最大值的计算不仅浪费而且在很多情况下并不稳定。以下为该感知器的代码:

class SurnameClassifier(nn.Module):
    """ 用于姓氏分类的两层多层感知器 """
    
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        初始化函数
        
        参数:
            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):
        """分类器的前向传播
        
        参数:
            x_in (torch.Tensor): 输入数据张量,形状应为 (batch, input_dim)
            apply_softmax (bool): 是否应用softmax激活函数的标志
                如果与交叉熵损失一起使用,应该设置为False
        返回:
            结果张量。张量形状应为 (batch, output_dim)
        """
        # 通过第一个全连接层并应用ReLU激活函数
        intermediate_vector = F.relu(self.fc1(x_in))
        # 通过第二个全连接层
        prediction_vector = self.fc2(intermediate_vector)

        # 如果apply_softmax为True,则应用softmax激活函数
        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)

        return prediction_vector

5. 训练例程Training Routine

       虽然我们使用了不同的模型、数据集和损失函数,但是训练例程是相同的。因此,下列代码只展示了args以及本例中的训练例程与“示例:餐厅评论情绪分类”中的示例之间的主要区别。

def set_seed_everywhere(seed, cuda):
    """设置所有随机数种子以确保可重复性
    
    参数:
        seed (int): 随机数种子
        cuda (bool): 是否使用CUDA
    """
    np.random.seed(seed)
    torch.manual_seed(seed)
    if cuda:
        torch.cuda.manual_seed_all(seed)

def handle_dirs(dirpath):
    """创建目录,如果目录不存在
    
    参数:
        dirpath (str): 目录路径
    """
    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,
    # 运行时选项
    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))

# 检查CUDA是否可用
if not torch.cuda.is_available():
    args.cuda = False

# 设置设备为CUDA或CPU
args.device = torch.device("cuda" if args.cuda else "cpu")
    
print("Using CUDA: {}".format(args.cuda))

# 设置随机数种子以确保可重复性
set_seed_everywhere(args.seed, args.cuda)

# 处理目录(创建保存模型的目录)
handle_dirs(args.save_dir)

6. 训练循环Training Loop

       与“Example: Classifying Sentiment of Restaurant Reviews”中的训练循环相比,本例的训练循环除了变量名以外几乎是相同的。具体来说,下列显示了使用不同的key从batch_dict中获取数据。除了外观上的差异,训练循环的功能保持不变。利用训练数据,计算模型输出、损失和梯度。然后,使用梯度来更新模型。

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,  # 当前epoch索引
            'train_loss': [],  # 训练损失记录
            'train_acc': [],  # 训练准确度记录
            'val_loss': [],  # 验证损失记录
            'val_acc': [],  # 验证准确度记录
            'test_loss': -1,  # 测试损失(初始化为-1)
            'test_acc': -1,  # 测试准确度(初始化为-1)
            'model_filename': args.model_state_file}  # 模型文件名

def update_train_state(args, model, train_state):
    """更新训练状态,包括提前停止和模型检查点"""
    # 保存第一个epoch的模型
    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

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

# 将分类器和权重移动到指定的设备
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)

# 开始训练循环
try:
    for epoch_index in range(args.num_epochs):
        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
        running_acc = 0.0
        classifier.train()

        for batch_index, batch_dict in enumerate(batch_generator):
            # 训练过程的五个步骤:

            # --------------------------------------
            # 步骤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()

        # 记录训练损失和准确度
        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()

        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.to("cpu").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()

        # 记录验证损失和准确度
        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")

7. 分类新姓氏

       以下代码是给定一个姓氏作为字符串,该函数将首先应用向量化过程,然后获得模型预测。注意,我们包含了apply_softmax标志,所以结果包含概率。模型预测,在多项式的情况下,是类概率的列表。我们使用PyTorch张量最大函数来得到由最高预测概率表示的最优类。

def predict_nationality(name, classifier, vectorizer):
    """
    根据姓名预测国籍

    Args:
        name (str): 需要预测的姓名
        classifier (SurnameClassifier): 已经训练好的分类器
        vectorizer (SurnameVectorizer): 用于将姓名向量化的向量化器

    Returns:
        dict: 包含预测的国籍和概率的字典
    """
    # 将姓名向量化
    vectorized_name = vectorizer.vectorize(name)
    # 将向量转换为torch张量,并调整形状为(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}
# 接收用户输入的姓氏
new_surname = input("输入要分类的姓氏: ")

# 将分类器转移到CPU上执行
classifier = classifier.to("cpu")

# 使用预测函数进行预测
prediction = predict_nationality(new_surname, classifier, vectorizer)

# 打印预测结果
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

测试结果:

输入要分类的姓氏:  McMahan
McMahan -> Irish (p=0.41)

8. 预测姓氏对应的国籍

       不仅要看最好的预测,还要看更多的预测。例如,NLP中的标准实践是采用k-best预测并使用另一个模型对它们重新排序。PyTorch提供了一个torch.topk函数,它提供了一种方便的方法来获得这些预测,如下列所示:

def predict_topk_nationality(name, classifier, vectorizer, k=5):
    """
    预测前k个可能的国籍及其概率

    Args:
        name (str): 需要预测的姓名
        classifier (SurnameClassifier): 已经训练好的分类器
        vectorizer (SurnameVectorizer): 用于将姓名向量化的向量化器
        k (int): 要返回的前k个预测结果,默认为5

    Returns:
        list: 包含前k个预测结果的字典列表
    """
    # 将姓名向量化
    vectorized_name = vectorizer.vectorize(name)
    # 将向量转换为torch张量,并调整形状为(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)
    
    # 将张量转换为NumPy数组,并取出第一个元素
    probability_values = probability_values.detach().numpy()[0]
    indices = indices.detach().numpy()[0]
    
    results = []
    # 遍历前k个预测结果,将国籍和概率组成字典并添加到结果列表中
    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("输入要分类的姓氏: ")
# 将分类器转移到CPU上执行
classifier = classifier.to("cpu")

# 获取用户想要查看的前k个预测结果数目
k = int(input("要查看前k个预测结果数目? "))
if k > len(vectorizer.nationality_vocab):
    print("抱歉!这超出了我们拥有的国籍数量.. 默认您查看最大数量的预测结果 :)")
    k = len(vectorizer.nationality_vocab)
    
# 进行预测
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)

# 打印前k个预测结果
print("前{}个预测结果:".format(k))
print("===================")
for prediction in predictions:
    print("{} -> {} (p={:0.2f})".format(new_surname,
                                        prediction['nationality'],
                                        prediction['probability']))

测试结果:

输入要分类的姓氏:  McMahan
要查看前k个预测结果数目?  5
前5个预测结果:
===================
McMahan -> Irish (p=0.41)
McMahan -> Scottish (p=0.25)
McMahan -> Czech (p=0.08)
McMahan -> Vietnamese (p=0.06)
McMahan -> German (p=0.05)

 

  • 28
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值