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

一、多层感知器

1.1什么是多层感知器?

1.2多层感知器的基本组成

1.3多层感知器的训练过程

1.4多层感知器与感知机的对比(XOR问题)

1.4.1解决方案对比

1.4.2输入输出对比

二、在PyTorch中实现MLP

2.1MLP定义

2.2实例化MLP

2.3测试模型的连接

2.4softmax的使用

三、基于MLP的姓氏分类

 3.1实验准备

3.1.1导入本次实验所使用的包

3.1.2数据集准备

3.2数据预处理

3.2.1词汇表的构建

3.2.2构建向量化器

3.2.3构建DataLoader

3.3构建MLP模型

3.4构建训练模型

3.4.1训练模型需要用到的一些函数

 3.4.2其他的一些函数

 3.4.3训练前准备和初始化

3.5模型循环训练

3.5.1常规训练

3.5.2使用MLP with dropout训练

(1)Dropout基本概念

(2)使用Dropout训练

3.6模型评估与预测

3.6.1新姓氏分类

3.6.2查询一个新姓氏的前K个预测

四、卷积神经网络

4.1什么是卷积神经网络?

4.1.1CNN的基本概念

4.1.2卷积的几个概念

(1)CHANNELS:

(2)KERNEL SIZE

(3)STRIDE

(5)DILATION

4.1.3CNN的架构

 4.2减小张量的方法

4.2.1构造特征向量

 4.2.2创建额外的卷积

4.2.3两种其他的方法

4.3NLP中卷积神经网络中的应用

 五、PyTorch中CNNs的实现

5.1SurnameDataset._getitem_的更改

5.2SurnameVectorizer的更改

5.3CNN-based SurnameClassifier模型的构建

 5.4CNN模型训练

5.5CNN模型评估

5.5.1新姓氏分类

5.5.2查询一个新姓氏前k个预测


一、多层感知器

 在介绍多层感知机之前,先简单的介绍以下感知机。

 感知机,也叫单层神经网络,是最基础的神经网络模型结构。周志华老师的西瓜书上对于感知机的定义为:“感知机由两层神经元组成,输入层接收外界输入信号后传递给输出层,输出层是M-P神经元。也称为阈值逻辑单元(threshold logic unit)”。以下为一个单层感知机模型示意图:

                                         

{x}_{1},{x}_{2},...,{x}_{i},...,{x}_{n}为输入层接收到的数据,{w}_{1},{w}_{2},...,{w}_{i},...,{w}_{n}为输出层M-P神经元模型的参数。经过训练数据集的训练,感知机可以自动地学习到阈值及权重。

1.1什么是多层感知器?

多层感知机(Multilayer Perceptron,简称MLP),是一种基于前馈神经网络(Feedforward Neural Network)的深度学习模型,除了输入输出层,它中间可以有多个隐层,其中每个神经元层与前一层全连接。

最简单的MLP:输入层—>隐藏层—>输出层

在MLP中,每一个神经元接收来自上一层的输入,这些输入会被加权并汇总,然后通过一个激活函数以产生该神经元的输出。这一过程模拟了生物神经元接收电信号并传递信号的机制。神经元的加权输入和激活函数的选择共同决定了网络的复杂性和能力。

1.2多层感知器的基本组成

  1. 神经元:包含一个带有权重和偏置的线性变换,以及一个激活函数(通常,输入层不使用激活函数,隐藏层和输出层使用激活函数)用来引入非线性,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以利用到更多的非线性模型中;
  2. 隐藏层神经元:假设输入层用向量X表示,则隐藏层的输出就是f({w}_{1}*x+{b}_{1}),函数f可以是函数softmax或者tanh函数,{w}_{1}是权重(连接系数),{b}_{1}是偏置;
  3. 输出层的输出:softmax({w}_{2}*{x}_{1}+{b}_{2}){x}_{1}是隐藏层的输出。

1.3多层感知器的训练过程

数据从输入层经过多个隐藏层的非线性变换,最后到达输出层进行分类或回归操作。通常使用反向传播算法,该算法通过计算损失函数对网络参数的梯度,并根据梯度更新参数,以最小化损失函数。常见的优化算法:随机梯度下降(SGD)、动量法、Adam等。

1.4多层感知器与感知机的对比(XOR问题)

主要得到的结论:感知机不能解决线性不可分问题,但MLP可以。

1.4.1解决方案对比

如上图所示,错误的分类用块填充,线是每个模型的决策边界。由左到右为感知机、含有两个隐层的MLP、含有三个隐层的MLP分类结果。可以发现,感知器学习—个不能正确地将圆与星分开的决策边界。但在在右动的面板中,MLP可以从圆中分离星,成功分类。从此可以得到MLP一个重要的优点:相对于单层感知机,多层感知机可以处理更复杂的非线性问题

1.4.2输入输出对比

感知器的输入和输出表示:

2-layer MLP的输入和输出表示:


 

3-layer MLP的输入和输出表示:

如上图所示, MLP的输入和中间表示是可视化的。对于2-layer MLP,从左到右为网络的输入、第一个线性模块的输出、第一个非线性模块的输出、第二个线性模块的输出。第一个线性模块的输出将圆和星分组,而第二个线性模块的输出将数据点重新组织为线性可分的。3-layer MLP与其大概一致,这里不做赘述。

但对于感知器的输入和输出,如最后一张图所示,感知器没有额外的一层来处理数据的形状,直到数据变成线性可分的。即因为它没有像MLP那样的中间表示来分组和重新组织,所以它不能将圆和星分开。

二、在PyTorch中实现MLP

实现环境:

python                           3.6.8

torch                              1.5.0

torchtext                        0.6.0

torchvision                     0.6.0

设备:CPU

2.1MLP定义

fc1定义了第一个全连接层,fc2定义了第二个全连接层,forward()定义了数据在模型中前向传播的过程。这样定义的两个全连接层构成了一个典型的两层全连接神经网络,可以用于各种需要向量到向量映射的任务。

import torch.nn as nn
import torch.nn.functional as F

class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        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):
        intermediate = F.relu(self.fc1(x_in))  # 第一个全连接层的输出,使用 ReLU 激活函数
        output = self.fc2(intermediate)   # 第二个全连接层的输出

        if apply_softmax:
            output = F.softmax(output, dim=1)   # 应用 softmax 函数
        return output

输入数据通过第一个全连接层fc1,然后应用 ReLU 激活函数得到中间结果。再将中间结果通过第二个全连接层fc2得到最终输出。

2.2实例化MLP

由于MLP实现的通用性,可以为任何大小的输入建模。为了演示,使用大小为3的输入维度、大小为4的输出维度和大小为100的隐藏维度。

batch_size = 2 # number of samples input at once
input_dim = 3
hidden_dim = 100
output_dim = 4
# Initialize model
mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
print(mlp)

运行结果:

 

在print语句的输出中,可以看到每个层中的单元数很好地排列在一起,以便为维度3的输入生成维度4的输出。

2.3测试模型的连接

可以通过传递一些随机输入来快速测试模型的“连接”。

import torch
def describe(x):
    print("Type: {}".format(x.type())) # 输出张量的类型
    print("Shape/size: {}".format(x.shape)) # 输出张量的形状/大小
    print("Values: \n{}".format(x))  # 输出张量的数值

x_input = torch.rand(batch_size, input_dim) # 创建一个随机张量
describe(x_input)

运行结果:

 

因为模型还没有经过训练,所以输出是随机的。在花费时间训练模型之前,这样做是一个有用的完整性检查。

y_output = mlp(x_input, apply_softmax=False)
describe(y_output)

运行结果:

如上所示,MLP模型的输出是一个有两行四列的张量。这个张量中的行与批处理维数对应,批处理维数是小批处理中的数据点的数量。列是每个数据点的最终特征向量。

2.4softmax的使用

上述模型输出的特征向量都是预测向量,名称为“预测向量”表示它对应于一个概率分布。预测向量会发生什么取决于我们当前是在进行训练还是在执行推理。在训练期间,输出按原样使用,带有一个损失函数和目标类标签的表示。

但是,如果想将预测向量转换为概率,需要softmax函数,softmax用于将一个值向量转换为概率。

y_output = mlp(x_input, apply_softmax=True)
describe(y_output)

 运行结果:

由运行结果,可见上述向量已被转换为概率。

三、基于MLP的姓氏分类

在本次实验中,我们将MLP应用于将姓氏分类到其原籍国的任务。从公开观察到的数据推断人口统计信息(如国籍)具有从产品推荐到确保不同人口统计用户获得公平结果的应用。

 3.1实验准备

3.1.1导入本次实验所使用的包

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
from tqdm.notebook import tqdm

3.1.2数据集准备

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

将其存放于上传至目录:/data/surnames/.中。

3.2数据预处理

3.2.1词汇表的构建

__init__:初始化变量;

to_serializable:返回一个可以被序列化的字典,包含了词汇表的信息:token_to_idx(单词到索引的映射)、add_unk(是否添加未知单词标记)、unk_token(未知单词的标记);

from_serializable:从一个序列化的字典内容中实例化Vocabulary类。即允许从已序列化的数据中重新构建词汇表对象;

add_token:将单词添加到词汇表中,并返回它的索引;

add_many:接受一个单词列表,通过循环调用add_token方法来将列表中的每个单词添加到词汇表中,并返回每个单词对应的索引列表;

look_token:根据输入的单词token查找其对应的索引;

lookup_index:根据输入的索引index查找其对应的单词;

__str__:返回一个描述性字符串,表示词汇表的大小;

__len__:返回token_to_idx字典的长度。

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>"):
        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):
        """ returns a dictionary that can be serialized """
        return {'token_to_idx': self._token_to_idx, 
                'add_unk': self._add_unk, 
                'unk_token': self._unk_token}

    @classmethod
    def from_serializable(cls, contents):
        """ instantiates the Vocabulary from a serialized dictionary """
        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)

以上提供了一种将文本数据中的单词映射到唯一索引的方法,并能够支持反向查找(从索引到单词)。add_token方法用于向词汇表中添加新的令牌,lookup_token方法用于检索索引,lookup_index方法用于检索给定索引的令牌(在推断阶段很有用)。

3.2.2构建向量化器

__init__:初始化一个SurnameVextorizer对象,接受两个参数,一个为姓氏的词汇表,用于存储所有可能的姓氏字符,另一个为国籍的词汇表,用于存储所有可能的国籍;

vectorize:将输入的姓氏surname转换为一个独热编码向量(one-hot encoding vector);

from_dataframe:通过从数据框surname_df中构建词汇表来创建SurnameVextorize对象;

from_serializable:从可序列化内容contens中重建 SurnameVextorize对象;

to_serializable:将 SurnameVextorize 对象序列化为一个字典,以便后续存储或传输。

class SurnameVectorizer(object):
    """ The Vectorizer which coordinates the Vocabularies and puts them to use"""
    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()}

 以上将姓氏及其对应的国籍转换为向量表示。它支持从数据框构建词汇表、序列化和反序列化操作,以及将姓氏转换为独热编码向量。

3.2.3构建DataLoader

__init__:初始化类,接受两个参数surname_df和vectorizer,根据surname_df中的split列分别提取训练集、验证集和测试集的子集 DataFrame。将训练集、验证集和测试集的大小存储下来,初始化时默认将当前数据集划分设为训练集,方便后续数据集访问。再进行类别权重计算;

load_dataset_and_make_vectorizer:加载数据集并创建向量化器,返回一个SurnameDataset实例;

load_vectorizer_only:从文件加载向量化器;

load_vectorizer_only:从文件加载向量化器的静态方法,用于在不创建数据集对象的情况下加载向量化器;

save_vectorizer:将 vectorizer对象保存到 JSON 文件中,以便将其用于加载和预处理数据;

get_vectorizer:返回当前数据集使用的vectorizer对象;

set_spilt:设置数据集的当前拆分,并根据选择的拆分更新数据集的目标数据帧和大小;

__len__:给出数据集的大小,即数据集中样本的数量;

__getitem__:根据给定的索引返回数据集中指定位置的数据。它首先从目标数据帧中获取该索引的行数据,然后使用 vectorizer对象将姓氏向量化,并查找国籍在词汇表中的索引。最后,以字典的形式返回姓氏向量和国籍索引;

generate_batches:利用 PyTorch 的Dataloader类来创建一个数据加载器dataloader,用于批处理数据集。

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)

    @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 load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
        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

以上generate_batches函数前实现了数据集的加载、划分和向量化,同时提供了类别权重的计算功能预处理,而 generate_batches 函数使用 PyTorch 的数据加载器将数据集分批次加载到指定的设备上,为神经网络的训练提供了方便的数据输入流程。

3.3构建MLP模型

根据前文以及提及到的MLP构建本次姓氏分类所需用到的MLP模型

class SurnameClassifier(nn.Module):
    """ A 2-layer Multilayer Perceptron for classifying surnames """
    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

输入数据通过第一个全连接层fc1,然后应用 ReLU 激活函数得到中间结果。再将中间结果通过第二个全连接层fc2得到最终输出。 

3.4构建训练模型

3.4.1训练模型需要用到的一些函数

  • make_train_state函数:创建并返回一个训练状态的字典,用于跟踪训练过程中的各种参数和指标;
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}
  • update_train_state函数:根据当前训练状态更新训练过程中的参数和控制早停策略。epoch_index等于0,即第一轮训练时,将当前模型的参数保存到指定的文件中。大于0时,记录最佳模型;
def update_train_state(args, model, train_state):
    # Save one model at least
    if train_state['epoch_index'] == 0:
        torch.save(model.state_dict(), train_state['model_filename'])
        train_state['stop_early'] = False

    # Save model if performance improved
    elif train_state['epoch_index'] >= 1:
        loss_tm1, loss_t = train_state['val_loss'][-2:]

        # If loss worsened
        if loss_t >= train_state['early_stopping_best_val']:
            # Update step
            train_state['early_stopping_step'] += 1
        # Loss decreased
        else:
            # Save the best model
            if loss_t < train_state['early_stopping_best_val']:
                torch.save(model.state_dict(), train_state['model_filename'])

            # Reset early stopping step
            train_state['early_stopping_step'] = 0

        # Stop early ?
        train_state['stop_early'] = \
            train_state['early_stopping_step'] >= args.early_stopping_criteria

    return train_state
  •  compute_accuracy函数:计算给定预测值y_pred和目标值y_target的准确率。
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

 3.4.2其他的一些函数

  •  set_seed_everywhere函数:设置随机数种子,以确保代码在每次运行时产生的随机数相同,从而使实验具有可重复性;
def set_seed_everywhere(seed, cuda):
    np.random.seed(seed)
    torch.manual_seed(seed)
    if cuda:
        torch.cuda.manual_seed_all(seed)
  •  handle_dirs函数:处理目录操作,首先检查给定的dirpath是否存在,如果不存在则创建该目录。
def handle_dirs(dirpath):
    if not os.path.exists(dirpath):
        os.makedirs(dirpath)

 3.4.3训练前准备和初始化

下面先定义args对象,用于存储各种配置和参数。再进行文件路径扩展,检查 CUDA 可用性以及设置设备类型,最后设置随机种子以确保随机操作的可重复性以及确保定义的目录存在,如果不存在则创建该目录。

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))
    
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_everywhere(args.seed, args.cuda)

handle_dirs(args.save_dir)

运行结果如下所示,可以看到拓展的路径,以及未使用GPU:

根据arg.reload_form_files的值加载数据集和向量化器,或者重新创建并保存向量化器。然后,基于加载或者新创建的数据集,准备用于训练的向量化器和分类器模型。

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))

运行结果如下,表示需创建向量化器:

3.5模型循环训练

以下首先实现将模型和数据移到指定的计算设备上进行训练,然后定义损失函数、优化器和学习率调度器,这些是模型训练过程中的关键组件,接着准备用于跟踪训练状态的对象或数据结构并创建一个在训练过程中显示进度的工具,方便监控训练进度。

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):
            optimizer.zero_grad()
            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)
            loss.backward()
            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")

3.5.1常规训练

迭代过程:

由上面迭代过程可以发现一轮训练大概耗时不到2s,我们可以打印出模型的loss以及accuary。

以下主要用于在训练过程中周期性地评估模型在测试数据集上的表现。它通过计算损失和准确率来衡量模型的泛化能力和预测精度。

# 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

print("Test loss: {};".format(train_state['test_loss']))
print("Test Accuracy: {}".format(train_state['test_acc']))

打印出模型的loss和accuracy为:

3.5.2使用MLP with dropout训练

(1)Dropout基本概念

DropOut:正常神经网络需要对每一个节点进行学习,而添加了DropOut的神经网络通过删除部分单元(随机),即暂时将其从网络中移除,以及它的所有传入和传出连接。将DropOut应用于神经网络相当于从神经网络中采样了一个“更薄的”网络,即单元个数较少。(如上图所示,DropOut是从左图采样了一个更薄的网络,如图右)在正反向传播的过程中,采样了多个稀薄网络,即Dropout可以解释为模型平均的一种形式。

Dropout能够有效缓解过拟合现象的发生,但是带来的缺点就是可能会减缓模型收敛的速度。

(2)使用Dropout训练

要使用dropout训练可以把上面定义的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 = F.relu(self.fc1(x_in)) # 使用 ReLU 激活函数
        output = self.fc2(F.dropout(intermediate, p=0.5)) #使用dropout

        if apply_softmax:
            output = F.softmax(output, dim=1) # 应用 softmax 激活函数

        return output

这里首先将输入数据传入第一个线性层fc1,然后通过 ReLU 激活函数处理,生成中间输出。再对中间输出 应用 Dropout 操作,Dropout 概率为0.5,这有助于模型的泛化能力和防止过拟合,最后将 Dropout 后的结果传入第二个线性层fx2,得到最终的输出out。

再次调用上面训练模型的函数运行可以看到迭代过程:

 可以发现验证集上的loss比上面未采用dropout的模型高,acc比上面模型低。可能是因为Dropout 可能导致模型在训练时无法充分拟合训练数据,因为每个训练步骤中都会随机关闭部分神经元。

3.6模型评估与预测

这部分只对初始模型做了评估与预测,因为dropout只适用于训练期间,不适用于评估期间。

3.6.1新姓氏分类

对于一个新姓氏,首先将输入的名字向量化,将向量化后的名字转换为 PyTorch 的张量,并调整其形状为一行多列的矩阵。这样做是为了与模型的输入格式匹配,最后再使用分类器预测概率分布(因为包含了apply_softmax标志所以为概率),获取最大的概率值以及它的索引,根据索引找到国籍即可。

def predict_nationality(name, classifier, vectorizer):
     # 将输入的名字向量化
    vectorized_name = vectorizer.vectorize(name)
    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("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']))

输入一个姓氏Vieas运行结果:

                                    

3.6.2查询一个新姓氏的前K个预测

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

def predict_topk_nationality(name, classifier, vectorizer, k=5):
    vectorized_name = vectorizer.vectorize(name)
    # 将向量化后的名字转换为 PyTorch 张量,并调整形状为一行多列的矩阵
    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)
    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("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']))

 输入一个姓氏Rihsa并打印出前五个预测结果:  
                                  

四、卷积神经网络

现在当我们听到神经网络(CNN)的时候,一般都会想到它在计算机视觉上的应用,这里我将说明CNN在NLP中的应用。

4.1什么是卷积神经网络?

4.1.1CNN的基本概念

简单来说,CNN就是包含非线性激活函数的多层卷积。在传统的前馈神经网络中,我们连接每一个输入的神经元,然后每个神经元输出给下一层,我们称之为全连接层。卷积神经网络中,则在输入层上使用卷积来计算输出。这导致了本地连接,每个输入区域都连接到输出中的一个神经元。每一层都使用不同的过滤器,通常是成百上千个,并结合了它们的结果。

4.1.2卷积的几个概念

(1)CHANNELS

通道(channel)是指沿输入中的每个点的特征维度。在图像中,对应于RGB组件的图像中的每个像素有三个通道。考虑到字符的卷积,通道的数量就是字符集的大小(在本次实验中刚好是词汇表)。在PyTorch卷积实现中,输入通道的数量是in_channels参数。卷积操作可以在输出(out_channels)中产生多个通道。可以将其视为卷积运算符将输入特征维“映射”到输出特征维。

如上图所示可以看到卷积运算用两个输入矩阵(两个输入通道)表示相应的核也有两层,它将每层分别相乘,然后对结果求和。

(2)KERNEL SIZE

核矩阵的宽度称为核大小(kernel_size)。通过增加核的大小,可以减少输出的大小。下图显示了一个大小为3的内核。卷积将输入中的空间(或时间)本地信息组合在一起,每个卷积的本地信息量由内核大小控制。

 可以看到当核大小为3时,输出矩阵是图中的2x2。

(3)STRIDE

Stride控制卷积之间的步长。如果步长与核相同,则内核计算不会重叠。另一方面,如果跨度为1,则内核重叠最大。输出张量可以通过增加步幅的方式被有意的压缩来总结信息。

 如上图应用于具有超参数步长的输入的kernel_size=2的卷积核等于2。这会导致内核采取更大的步骤,从而产生更小的输出矩阵。对于更稀疏地对输入矩阵进行二次采样非常有用。

(4)PADDING

即使stride和kernel_size允许控制每个计算出的特征值有多大范围,它们也有一个有害的、有时是无意的副作用,那就是缩小特征映射的总大小(卷积的输出)。因此引入了PADDING,方法是在每个维度上附加和前置0。这意味着CNN将执行更多的卷积,但是输出形状可以控制,而不会影响所需的核大小、步幅或扩展。

 如图应用于高度和宽度等于的输入矩阵的kernel_size=2的卷积2。但是,由于填充(用深灰色正方形表示),输入矩阵的高度和宽度可以被放大。这通常与大小为3的内核一起使用,这样输出矩阵将等于输入矩阵的大小。

(5)DILATION

 膨胀控制卷积核有点类似于stride,但它是对 kernel 进行膨胀,多出来的空隙用 0 padding。用于克服 stride 中造成的失真问题。

 如图为应用于超参数dilation=2的输入矩阵的kernel_size=2的卷积。从默认值开始膨胀的增加意味着核矩阵的元素在与输入矩阵相乘时进一步分散开来。进一步增大扩张会加剧这种扩散。

4.1.3CNN的架构

由上图一个实例CNN基本结构:INPUT -> 卷积->激活 -> 池化-> 全连接 ->OUTPUT

卷积层(Convolutional Layer)
这是CNN的核心,卷积层使用一组可学习的滤波器来捕获输入数据的局部特征。每个滤波器在原始图像上滑动进行卷积操作;

池化层(Pooling Layer)
池化操作跟在卷积层之后,其目的是降维,从而减少参数的数量和计算的复杂性。通过保留每个窗口中的最大值,池化层不仅降低了过拟合的风险,还提高了模型的空间不变性。

全连接层(Fully Connected Layer)
经过一系列的卷积和池化层之后,所学习的高级特征被展平并送入全连接层。全连接层的作用是将这些特征映射到最终的输出类别。在这里,网络将进行最后的决策,输出最终的分类结果。
 

 4.2减小张量的方法

4.2.1构造特征向量

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

batch_size = 2  
one_hot_size = 10  
sequence_width = 7  
data = torch.randn(batch_size, one_hot_size, sequence_width)  # 生成一个随机张量
conv1 = Conv1d(in_channels=one_hot_size, out_channels=16,  # 创建一个 Conv1d 层,输入通道数为 one_hot_size,输出通道数为 16
               kernel_size=3)  
intermediate1 = conv1(data)  
print(data.size()) 
print(intermediate1.size())  # 打印第一个卷积层输出的大小

运行结果:

 

 4.2.2创建额外的卷积

conv2 = nn.Conv1d(in_channels=16, out_channels=32, kernel_size=3)  # 创建第二个 Conv1d 层,输入通道数为 16,输出通道数为 32,卷积核大小为 3
conv3 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3)  # 创建第三个 Conv1d 层,输入通道数为 32,输出通道数为 64,卷积核大小为 3

intermediate2 = conv2(intermediate1)  # 将中间结果 intermediate1 传递给第二个卷积层
intermediate3 = conv3(intermediate2)  # 将中间结果 intermediate2 传递给第三个卷积层

print(intermediate2.size())  # 打印第二个卷积层输出的大小
print(intermediate3.size())  
y_output = intermediate3.squeeze()
print(y_output.size())

运行结果: 

  

 在每次卷积中,通道维数的大小都会增加,因为通道维数是每个数据点的特征向量。最终的输出在最终维度上的大小为1。

4.2.3两种其他的方法

这两种方法可以将张量简化为每个数据点的一个特征向量:将剩余的值压平为特征向量,并在额外维度上求平均值。第一种方法,只需使用PyTorch的view()方法将所有向量平展成单个向量。第二种方法使用一些数学运算来总结向量中的信息。

# Method 2 of reducing to feature vectors(将中间结果视图重塑为特征向量)
print(intermediate1.view(batch_size, -1).size())

# Method 3 of reducing to feature vectors(通过取平均值将中间结果减少为特征向量)
print(torch.mean(intermediate1, dim=2).size())
#通过取最大值将中间结果减少为特征向量
max_values, _ = torch.max(intermediate1, dim=2)
print(max_values.size())
print(torch.sum(intermediate1, dim=2).size())

运行结果:

 

这种设计一系列卷积的方法是基于经验的:从数据的预期大小开始,处理一系列卷积,最终得到适合的特征向量。

4.3NLP中卷积神经网络中的应用

文本分类:CNN可以通过卷积操作和池化操作来捕捉文本中的局部特征,如词组合和短语,从而有效地进行文本分类任务。

情感分析:在情感分析任务中,CNN可以帮助识别和学习文本中情感表达的模式和特征。通过卷积和池化层,CNN可以有效地捕捉情感表达中的重要词语或短语,从而进行情感判断。

文本生成:在文本生成任务中,CNN通常用作生成模型中的编码器部分,帮助将文本序列转换为语义空间中的表示。通过卷积神经网络,可以有效地学习文本中的局部特征和语义信息,从而提高生成模型的性能。

语言建模:CNN可以用于语言建模任务,例如预测下一个单词或者生成连续的文本序列。通过卷积层,CNN可以从文本中提取各种尺度的特征,帮助理解和建模文本的语言结构和上下文关系。

 

 五、PyTorch中CNNs的实现

在前文已经介绍了卷积神经网络,这里就不赘述。

这里的实现的许多细节与前面的MLP示例相同,只有部分地方需要修改,下面我会介绍。但真正发生变化的是模型的构造和向量化过程。

5.1SurnameDataset._getitem_的更改

实现了一个数据集类,它跟踪最长的姓氏,并将其作为矩阵中包含的行数提供给矢量化器。列的数量是onehot向量的大小(词汇表的大小)。

class SurnameDataset(Dataset):
    # ... existing implementation from Section 4.2

    def __getitem__(self, index):
        row = self._target_df.iloc[index]

        # 使用 vectorizer 对姓氏进行向量化,并限制序列长度为最大序列长度
        surname_matrix = \
            self._vectorizer.vectorize(row.surname, self._max_seq_length)

        # 使用 vectorizer 中的 nationality_vocab 查找国籍对应的索引
        nationality_index = \
             self._vectorizer.nationality_vocab.lookup_token(row.nationality)

        return {'x_surname': surname_matrix,  
                'y_nationality': nationality_index}  

这里我们使用数据集中最长的姓氏来控制onehot矩阵的大小有两个原因。首先,将每一小批姓氏矩阵组合成一个三维张量,要求它们的大小相同。其次,使用数据集中最长的姓氏意味着可以以相同的方式处理每个小批处理。

5.2SurnameVectorizer的更改

除了更改为使用onehot矩阵之外,还需要修改矢量化器,以便计算姓氏的最大长度并将其保存为max_surname_length。

class SurnameVectorizer(object):
    """ The Vectorizer which coordinates the Vocabularies and puts them to use"""
    def vectorize(self, surname):
        # 创建一个全零矩阵,矩阵的大小为字符词汇表的长度乘以最大姓氏长度
        one_hot_matrix_size = (len(self.character_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.character_vocab.lookup_token(character)
            one_hot_matrix[character_index][position_index] = 1

        return one_hot_matrix
    
    @classmethod
    def from_dataframe(cls, surname_df):
        # 创建字符词汇表和国籍词汇表
        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)

该函数将字符串中的每个字符映射到一个整数,然后使用该整数构造一个由onehot向量组成的矩阵。重要的是,矩阵中的每一列都是不同的onehot向量。主要原因是,即将使用的Conv1d层要求数据张量在第0维上具有批处理,在第1维上具有通道,在第2维上具有特性。

5.3CNN-based SurnameClassifier模型的构建

使用nn.Sequential定义了一系列的卷积层和激活函数ELU。nn.Conv1d是一维卷积层,适用于处理序列数据,比如文本数据。使用nn.ELU激活函数,有助于减少梯度消失问题。

全连接层fc:将卷积层提取的特征映射到类别数量的输出。

forward(前向传播方法):将输入数据通过卷积神经网络self.convnet进行特征提取。.squeeze(dim=2)用于去除维度为1的维度,保证特征向量的形状正确。self.fc(features)将卷积层提取的特征features输入全连接层进行分类预测。

class SurnameClassifier(nn.Module):
    def __init__(self, initial_num_channels, num_classes, num_channels):
        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):
        # 卷积部分
        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

以上构建了处理姓氏数据的分类任务的CNN模型,通过卷积神经网络提取特征,并通过全连接层输出预测结果。 

 5.4CNN模型训练

具体的训练过程与前面相同,但是,输入参数是不同的。参数修改如下所示:

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/cnn",
    # Model hyper parameters
    hidden_dim=100,
    num_channels=256,
    # Training hyper parameters
    seed=1337,
    learning_rate=0.001,
    batch_size=128,
    num_epochs=100,
    early_stopping_criteria=5,
    dropout_p=0.1,
    # Runtime omitted for space ...
)

 训练迭代过程如下所示:

计算出在测试集上的loss与acc为:

可以看到它在acc比MLP模型有所提高。

5.5CNN模型评估

5.5.1新姓氏分类

predict_nationality()函数的更改:没有使用视图方法重塑新创建的数据张量以添加批处理维度,而是使用PyTorch的unsqueeze()函数在批处理应该在的位置添加大小为1的维度。

def predict_nationality(surname, classifier, vectorizer):
    # 将姓氏矢量化
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(0)
    
    # 使用分类器进行预测
    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}

 输入姓氏进行判断:

5.5.2查询一个新姓氏前k个预测

在predict_topk_nationality函数中做和上述相同的更改如下所示:

def predict_nationality(surname, classifier, vectorizer):

    # 将姓氏矢量化
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(0)
    
    # 使用分类器进行预测
    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}

 输入姓氏进行判断前5个预测: 

 

  • 33
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值