使用感知机与卷积神经网络处理姓氏分类问题

目录

1. 多层感知机

1.1 原理介绍

1.2 激活函数

        1.2.1 ReLU函数

        1.2.2 sigmoid函数

        1.2.3 tanh函数

1.3 使用多层感知机处理姓氏分类问题

        1.3.1 The Surname Dataset

        1.3.2 Vocabulary, Vectorizer, and DataLoader

        THE VOCABULARY CLASS

        THE SURNAMEVECTORIZER

        1.3.3 The Surname Classifier Model

        1.3.4 The Training Routine

        THE TRAINING LOOP

        1.3.5 Model Evaluation and Prediction 

        EVALUATING ON THE TEST DATASET

        CLASSIFYING A NEW SURNAME

        RETRIEVING THE TOP-K PREDICTIONS FOR A NEW SURNAME

        1.3.6 Regularizing MLPs: Weight Regularization and Structural Regularization (or Dropout)

        DROPOUT

        1.3.7 运行结果

2. 卷积神经网络

2.1 原理介绍

        2.1.1卷积神经网络结构——以LnNet为例

        2.1.2 卷积层

        2.1.3 激活层

        2.1.4 池化层

2.2 使用卷积神经网络处理姓氏分类问题

        2.2.1 The SurnameDataset

        2.2.2 Vocabulary, Vectorizer, and DataLoader

        2.2.3 Reimplementing the SurnameClassifier with Convolutional Networks

        2.2.4 The Training Routine     

        2.2.5 Model Evaluation and Prediction

         Evaluating on the Test Dataset

        Classifying or retrieving top predictions for a new surname

        2.2.6 运行结果


1. 多层感知机

1.1 原理介绍

        多层感知机(Multilayer Perceptron, MLP)是一种基本的人工神经网络模型,它由多个神经元层组成,每一层与下一层全连接。MLP通常包括输入层、若干个隐藏层(中间层),以及一个输出层。每一层中的神经元都与下一层的每个神经元连接,通过带权重的连接进行信息传递和处理。

        最简单的MLP,如图所示,由三个表示阶段和两个线性层组成。第一阶段是输入向量。这是给定给模型的向量。

1.2 激活函数

        线性意味着单调假设:任何特征的增大都会导致模型输出的增大(如果对应的权重为正),或者导致模 型输出的减小(如果对应的权重为负)。有时这是有道理的。例如,如果我们试图预测一个人是否会偿还贷款。 我们可以认为,在其他条件不变的情况下,收入较高的申请人比收入较低的申请人更有可能偿还贷款。但是, 虽然收入与还款概率存在单调性,但它们不是线性相关的。收入从0增加到5万,可能比从100万增加到105万 带来更大的还款可能性。处理这一问题的一种方法是对我们的数据进行预处理,使线性变得更合理,如使用 收入的对数作为我们的特征。

        激活函数(activationfunction)通过计算加权和并加上偏置来确定神经元是否应该被激活,它们将输入信号 转换为输出的可微运算。大多数激活函数都是非线性的。由于激活函数是深度学习的基础,下面简要介绍一 些常见的激活函数。

        1.2.1 ReLU函数

        ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

        当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。下面是ReLU函数的导数。

y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))

        1.2.2 sigmoid函数

        对于一个定义域在R中的输入,sigmoid函数将输入变换为区间(0,1)上的输出。

y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))

        sigmoid函数的导数图像如下所示。

y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))

        1.2.3 tanh函数

        tanh(双曲正切)函数也能将其输入压缩转换到区间(‐1,1)上。

y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))

        tanh函数的导数图像如下所示。

y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))

1.3 使用多层感知机处理姓氏分类问题

        这一示例中,我们将使用多层感知器(Multilayer Perceptron, MLP)来进行姓氏分类。多层感知器是一种前馈神经网络,通过多个隐藏层进行非线性变换,可以有效地处理复杂的分类任务。

        我们首先对每个姓氏的字符进行拆分,并像对待“示例:将餐馆评论的情绪分类”中的单词一样对待它们。除了数据上的差异,字符层模型在结构和实现上与基于单词的模型基本相似。然后,我们使用词汇表、向量化器和DataLoader类逐步完成从姓氏字符串到向量化小批处理的管道。

        我们将通过描述姓氏分类器模型及其设计背后的思想过程来继续本节。除了模型的改变,我们在这个例子中引入了多类输出及其对应的损失函数。在描述了模型之后,我们完成了训练例程。

        1.3.1 The 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):
        """
        Args:
            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):
        """加载数据集并从头创建一个新的向量化器
        
        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:
            SurnameVectorizer的一个实例
        """
        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_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):
        """给定批大小,返回数据集中的批次数
        
        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。确保每个张量在正确的设备位置上。
    """
    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
args = Namespace(
    raw_dataset_csv="data/surnames/surnames.csv",
    train_proportion=0.7,
    val_proportion=0.15,
    test_proportion=0.15,
    output_munged_csv="data/surnames/surnames_with_splits.csv",
    seed=1337
)
        1.3.2 Vocabulary, Vectorizer, and DataLoader


        为了使用字符对姓氏进行分类,我们使用词汇表、向量化器和DataLoader将姓氏字符串转换为向量化的minibatches。这些数据结构与“Example: Classifying Sentiment of Restaurant Reviews”中使用的数据结构相同,它们举例说明了一种多态性,这种多态性将姓氏的字符标记与Yelp评论的单词标记相同对待。数据不是通过将字令牌映射到整数来向量化的,而是通过将字符映射到整数来向量化的。

        THE VOCABULARY CLASS

        词汇表是两个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>"):
        """
        初始化函数
        
        Args:
            token_to_idx (dict): token到索引的映射字典
            add_unk (bool): 是否添加UNK(未知)标记的标志
            unk_token (str): 要添加到词汇表中的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):
        """返回一个可序列化的字典"""
        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关联的索引或UNK索引(如果token不存在)
        
        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)
        else:
            return self._token_to_idx[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("索引(%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)
        THE SURNAMEVECTORIZER

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

下面给出简化代码:

class SurnameVectorizer(object):
    """协调词汇表并将其应用于向量化的向量器"""

    def __init__(self, surname_vocab, nationality_vocab):
        """
        初始化向量器
        
        Args:
            surname_vocab (Vocabulary): 将字符映射到整数的词汇表
            nationality_vocab (Vocabulary): 将国籍映射到整数的词汇表
        """
        self.surname_vocab = surname_vocab
        self.nationality_vocab = nationality_vocab

    def vectorize(self, surname):
        """
        向量化姓氏
        
        Args:
            surname (str): 姓氏字符串

        Returns:
            one_hot (np.ndarray): 折叠的一位热编码表示 
        """
        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)

    @classmethod
    def from_serializable(cls, contents):
        """从可序列化内容实例化向量器
        
        Args:
            contents (dict): 包含 'surname_vocab' 和 'nationality_vocab' 的序列化词汇表

        Returns:
            SurnameVectorizer 的一个实例
        """
        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):
        """
        将向量器序列化为可保存的字典表示
        
        Returns:
            dict: 包含 'surname_vocab' 和 'nationality_vocab' 的可序列化内容
        """
        return {'surname_vocab': self.surname_vocab.to_serializable(),
                'nationality_vocab': self.nationality_vocab.to_serializable()}
        1.3.3 The Surname Classifier Model

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

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

下面给出简化代码:

class SurnameClassifier(nn.Module):
    """
    用于对姓氏进行分类的 2 层多层感知器
    """
    def __init__(self, input_dim, hidden_dim, output_dim):
        """
        初始化 SurnameClassifier 类

        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 应为 (batch, input_dim)
            apply_softmax (bool): softmax 激活的标志
                如果与交叉熵损失一起使用,应为 False
        Returns:
            结果张量。tensor.shape 应为 (batch, output_dim)
        """
        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
1.3.4 The Training Routine

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

args = Namespace(
    # 数据和路径信息
    surname_csv="data/surnames/surnames_with_splits.csv",  # 姓氏数据的CSV文件路径
    vectorizer_file="vectorizer.json",  # 向量化器保存路径
    model_state_file="model.pth",  # 模型状态保存路径
    save_dir="model_storage/ch4/surname_mlp",  # 模型检查点保存目录
    
    # 模型超参数
    hidden_dim=300,  # MLP中隐藏层的维度
    
    # 训练超参数
    seed=1337,  # 随机种子,用于可重复性
    num_epochs=100,  # 训练的总epoch数
    early_stopping_criteria=5,  # 提前停止的标准:连续多少个epoch验证集损失没有改善
    learning_rate=0.001,  # 优化器的学习率
    batch_size=64,  # 训练时的批量大小

)

        训练中最显著的差异与模型中输出的种类和使用的损失函数有关。在这个例子中,输出是一个多类预测向量,可以转换为概率。正如在模型描述中所描述的,这种输出的损失类型仅限于CrossEntropyLoss和NLLLoss。由于它的简化,我们使用了CrossEntropyLoss。

        在此,我们展示了数据集、模型、损失函数和优化器的实例化。这些实例应该看起来与“示例:将餐馆评论的情绪分类”中的实例几乎相同。事实上,在本课程后面的实验中,这种模式将对每个示例进行重复。

        

dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)  # 加载数据集并创建向量化器
vectorizer = dataset.get_vectorizer()  # 获取向量化器

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)  # 使用交叉熵损失函数,并考虑数据集类别不均衡的权重
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)  # 使用Adam优化器,并传入分类器参数和学习率
        THE TRAINING LOOP

        示例显示了使用不同的key从batch_dict中获取数据。除了外观上的差异,训练循环的功能保持不变。利用训练数据,计算模型输出、损失和梯度。然后,使用梯度来更新模型。

# 将模型和类权重移动到指定设备
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.0
        running_acc = 0.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")
        1.3.5 Model Evaluation and Prediction 


        要理解模型的性能,应该使用定量和定性方法分析模型。定量测量出的测试数据的误差,决定了分类器能否推广到不可见的例子。定性地说,可以通过查看分类器的top-k预测来为一个新示例开发模型所了解的内容的直觉。

        EVALUATING ON THE TEST DATASET


        该模型对测试数据的准确性达到50%左右。如果在附带的notebook中运行训练例程,会注意到在训练数据上的性能更高。这是因为模型总是更适合它所训练的数据,所以训练数据的性能并不代表新数据的性能。如果遵循代码,你可以尝试隐藏维度的不同大小,应该注意到性能的提高。然而,这种增长不会很大(尤其是与“用CNN对姓氏进行分类的例子”中的模型相比)。其主要原因是收缩的onehot向量化方法是一种弱表示。虽然它确实简洁地将每个姓氏表示为单个向量,但它丢弃了字符之间的顺序信息,这对于识别起源非常重要。 

        CLASSIFYING A NEW SURNAME


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

部分代码展示:

def predict_nationality(name, classifier, vectorizer):
    # 向量化输入的姓名
    vectorized_name = vectorizer.vectorize(name)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    
    # 使用分类器进行预测
    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}
        RETRIEVING THE TOP-K PREDICTIONS FOR A NEW SURNAME

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

def predict_topk_nationality(name, classifier, vectorizer, k=5):
    # 向量化输入的姓名
    vectorized_name = vectorizer.vectorize(name)
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)
    
    # 使用分类器进行预测并获取前k个最高概率的国籍
    prediction_vector = classifier(vectorized_name, apply_softmax=True)
    probability_values, indices = torch.topk(prediction_vector, k=k)

    # 将概率值和索引转换为numpy数组
    probability_values = probability_values.detach().numpy()[0]
    indices = indices.detach().numpy()[0]

    results = []
    # 根据索引查找国籍,并保存每个国籍及其对应的概率值
    for prob_value, index in zip(probability_values, indices):
        nationality = vectorizer.nationality_vocab.lookup_index(index)
        results.append({'nationality': nationality,
                        'probability': prob_value})

    return results
        1.3.6 Regularizing MLPs: Weight Regularization and Structural Regularization (or Dropout)

       正则化可以解决过拟合问题,常见的重要权重正则化类型有L1和L2两种。这些权值正则化方法也适用于MLPs和卷积神经网络。除权值正则化外,对于深度模型(即例如本实验讨论的前馈网络,一种称为dropout的结构正则化方法变得非常重要。

        DROPOUT

        简单地说,在训练过程中,dropout有一定概率使属于两个相邻层的单元之间的连接减弱。这有什么用呢?我们从斯蒂芬•梅里蒂(Stephen Merity)的一段直观(且幽默)的解释开始:“Dropout,简单地说,是指如果你能在喝醉的时候反复学习如何做一件事,那么你应该能够在清醒的时候做得更好。这一见解产生了许多最先进的结果和一个新兴的领域。”

        神经网络——尤其是具有大量分层的深层网络——可以在单元之间创建有趣的相互适应。“Coadaptation”是神经科学中的一个术语,但在这里它只是指一种情况,即两个单元之间的联系变得过于紧密,而牺牲了其他单元之间的联系。这通常会导致模型与数据过拟合。通过概率地丢弃单元之间的连接,我们可以确保没有一个单元总是依赖于另一个单元,从而产生健壮的模型。dropout不会向模型中添加额外的参数,但是需要一个超参数——“drop probability”。drop probability,它是单位之间的连接drop的概率。通常将下降概率设置为0.5:

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 应为 (batch, input_dim)
            apply_softmax (bool): softmax激活的标志
                如果与交叉熵损失一起使用,应为False

        Returns:
            torch.Tensor: 输出张量。tensor.shape 应为 (batch, output_dim)
        """
        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

        注意,dropout只适用于训练期间,不适用于评估期间。作为练习,可以尝试带有dropout的SurnameClassifier模型,看看它如何更改结果。

        1.3.7 运行结果

        

2. 卷积神经网络

2.1 原理介绍

        卷积神经网络(Convolutional Neural Network,CNN)是一种特殊的人工神经网络,主要用于处理具有网格状结构数据,如图像和视频。CNN 的关键特点是通过卷积层提取输入数据的空间结构特征,并通过池化层减少数据维度,从而降低模型复杂度和计算量。它的结构灵感来自生物视觉皮层的工作方式,适用于许多视觉任务,如图像分类、对象检测和语义分割。CNN 的设计使得它在处理大规模高维数据时效果显著,成为计算机视觉领域的重要工具。

        严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross‐correlation),而不是卷 积运算。

        在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。当卷积窗口滑动到新 一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的 标量值,由此我们得出了这一位置的输出张量值。在如上例子中,输出张量的四个元素由二维互相关运算得 到,这个输出高度为2、宽度为2,如下所示:

0×0 + 1×1 + 3×2 + 4×3=19

1×0 + 2×1 + 4×2 + 5×3=25

3×0 + 4×1 + 6×2 + 7×3=37

4×0 + 5×1 + 7×2 + 8×3=43

        2.1.1卷积神经网络结构——以LnNet为例

        LeNet是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关 注,这个模型是由AT&T贝尔实验室的研究员YannLeCun在1989年提出的。

        总体来看,LeNet(LeNet‐5)由两个部分组成: • 卷积编码器:由两个卷积层组成; • 全连接层密集块:由三个全连接层组成。

        每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层。请注意,虽然ReLU和最大汇 聚层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用5×5卷积核和一个sigmoid激活函数。这 些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷 积层有16个输出通道。每个2×2池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大 小、通道数、高度、宽度决定。

        为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换 成全连接层所期望的二维输入。这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样 本的平面向量表示。LeNet的稠密块有三个全连接层,分别有120、84和10个输出。因为我们在执行分类任务, 所以输出层的10维对应于最后输出结果的数量。

        

        2.1.2 卷积层

        卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。

        卷积核可以通过以下参数进行调整:

        大小(Size): 卷积核的尺寸通常是一个正方形或矩形,可以通过指定宽度和高度来定义。

        步长(Stride): 定义卷积核在输入数据上移动的步长。较大的步长会减少输出的空间维度。

        填充(Padding): 在输入数据的边缘周围添加额外的像素,通常使用零填充(zero-padding),以便在应用卷积核时可以保持输入和输出的尺寸一致或达到所需的维度。

        通道数(Channels): 对于多通道输入(如彩色图像),卷积核的深度必须与输入数据的通道数匹配。

        初始化方法(Initialization Method): 卷积核的初始权重可以使用不同的初始化方法,如随机初始化、Xavier 初始化或 He 初始化,以便更好地训练神经网络。

        2.1.3 激活层

        把卷积层输出结果做非线性映射,与1.2节相同。

        2.1.4 池化层

        池化层夹在连续的卷积层中间, 用于压缩数据和参数的量,减小过拟合。如果输入是图像的话,那么池化层的最主要作用就是压缩图像,有如下好处:降维与减少计算量、防止过拟合、增强特征的鲁棒性。
        常见的池化操作:

                最大池化(Max Pooling):池化窗口内的最大值作为输出,保留了最显著的特征。

                平均池化(Average Pooling):池化窗口内所有值的平均值作为输出,保留整体特征。

2.2 使用卷积神经网络处理姓氏分类问题

        为了证明CNN的有效性,让我们应用一个简单的CNN模型来分类姓氏。这项任务的许多细节与前面的MLP示例相同,但真正发生变化的是模型的构造和向量化过程。模型的输入,而不是我们在上一个例子中看到的收缩的onehot,将是一个onehot的矩阵。这种设计将使CNN能够更好地“view”字符的排列,并对在“示例:带有多层感知器的姓氏分类”中使用的收缩的onehot编码中丢失的序列信息进行编码。

        2.2.1 The SurnameDataset

        虽然姓氏数据集之前在“使用多层感知机处理姓氏分类问题”中进行了描述,但建议参考“姓氏数据集”来了解它的描述。尽管我们使用了来自“使用多层感知机处理姓氏分类问题”中的相同数据集,但在实现上有一个不同之处:数据集由onehot向量矩阵组成,而不是一个收缩的onehot向量。为此,我们实现了一个数据集类,它跟踪最长的姓氏,并将其作为矩阵中包含的行数提供给矢量化器。列的数量是onehot向量的大小(词汇表的大小)。

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

class SurnameDataset(Dataset):
    def __init__(self, surname_df, vectorizer):
        """
        Args:
            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):
        """加载数据集并创建一个新的向量化器
        
        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。它将确保每个张量位于正确的设备位置。
    """
    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.2.2 Vocabulary, Vectorizer, and DataLoader

        在本例中,尽管词汇表和DataLoader的实现方式与“使用多层感知机处理姓氏分类问题”中的示例相同,但Vectorizer的vectorize()方法已经更改,以适应CNN模型的需要。具体来说,正如我们在示例中的代码中所示,该函数将字符串中的每个字符映射到一个整数,然后使用该整数构造一个由onehot向量组成的矩阵。重要的是,矩阵中的每一列都是不同的onehot向量。主要原因是,我们将使用的Conv1d层要求数据张量在第0维上具有批处理,在第1维上具有通道,在第2维上具有特性。

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

部分代码展示:

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标记的标志
            unk_token (str): 要添加到词汇表中的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):
        """返回一个可序列化的字典"""
        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 (已添加到Vocabulary中)以实现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)
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)
            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的一个实例
        """
        surname_vocab = Vocabulary(unk_token="@")
        nationality_vocab = Vocabulary(add_unk=False)
        max_surname_length = 0

        for index, row in surname_df.iterrows():
            max_surname_length = max(max_surname_length, len(row.surname))
            for letter in row.surname:
                surname_vocab.add_token(letter)
            nationality_vocab.add_token(row.nationality)

        return cls(surname_vocab, nationality_vocab, max_surname_length)

    @classmethod
    def from_serializable(cls, contents):
        surname_vocab = Vocabulary.from_serializable(contents['surname_vocab'])
        nationality_vocab =  Vocabulary.from_serializable(contents['nationality_vocab'])
        return cls(surname_vocab=surname_vocab, nationality_vocab=nationality_vocab, 
                   max_surname_length=contents['max_surname_length'])

    def to_serializable(self):
        return {'surname_vocab': self.surname_vocab.to_serializable(),
                'nationality_vocab': self.nationality_vocab.to_serializable(), 
                'max_surname_length': self._max_surname_length}
        2.2.3 Reimplementing the SurnameClassifier with Convolutional Networks

        我们在本例中使用的模型是使用我们在“卷积神经网络”中介绍的方法构建的。实际上,我们在该部分中创建的用于测试卷积层的“人工”数据与姓氏数据集中使用本例中的矢量化器的数据张量的大小完全匹配。正如在示例中所看到的,它与我们在“卷积神经网络”中引入的Conv1d序列既有相似之处,也有需要解释的新添加内容。具体来说,该模型类似于“卷积神经网络”,它使用一系列一维卷积来增量地计算更多的特征,从而得到一个单特征向量。

        然而,本例中的新内容是使用sequence和ELU PyTorch模块。序列模块是封装线性操作序列的方便包装器。在这种情况下,我们使用它来封装Conv1d序列的应用程序。ELU是类似于实验3中介绍的ReLU的非线性函数,但是它不是将值裁剪到0以下,而是对它们求幂。ELU已经被证明是卷积层之间使用的一种很有前途的非线性(Clevert et al., 2015)。

        在本例中,我们将每个卷积的通道数与num_channels超参数绑定。我们可以选择不同数量的通道分别进行卷积运算。这样做需要优化更多的超参数。我们发现256足够大,可以使模型达到合理的性能。

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 应为 (batch, initial_num_channels, max_surname_length)
            apply_softmax (bool): softmax激活的标志
                如果与交叉熵损失一起使用,则应为False
        Returns:
            torch.Tensor: 输出张量,tensor.shape 应为 (batch, num_classes)
        """
        # 经过卷积神经网络层
        features = self.convnet(x_surname).squeeze(dim=2)
        
        # 经过全连接层
        prediction_vector = self.fc(features)

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

        return prediction_vector
        2.2.4 The Training Routine     

        训练程序包括以下似曾相识的的操作序列:实例化数据集,实例化模型,实例化损失函数,实例化优化器,遍历数据集的训练分区和更新模型参数,遍历数据集的验证分区和测量性能,然后重复数据集迭代一定次数。此时,这是本书到目前为止的第三个训练例程实现,应该将这个操作序列内部化。对于这个例子,我们将不再详细描述具体的训练例程,因为它与“示例:带有多层感知器的姓氏分类”中的例程完全相同。但是,输入参数是不同的,可以在示例中看到。

args = Namespace(
    # 数据和路径信息
    surname_csv="data/surnames/surnames_with_splits.csv",  # 姓氏数据集CSV文件路径
    vectorizer_file="vectorizer.json",  # 向量化器保存路径
    model_state_file="model.pth",  # 模型状态保存路径
    save_dir="model_storage/ch4/cnn",  # 模型保存目录
    # 模型超参数
    hidden_dim=100,  # 隐藏层维度
    num_channels=256,  # 卷积核数量
    # 训练超参数
    seed=1337,  # 随机种子
    learning_rate=0.001,  # 学习率
    batch_size=128,  # 批大小
    num_epochs=100,  # 训练轮数
    early_stopping_criteria=5,  # 提前停止训练的条件
    dropout_p=0.1,  # Dropout概率
    # 运行时参数已省略,以节省空间 ...
)
        2.2.5 Model Evaluation and Prediction

        要理解模型的性能,需要对性能进行定量和定性的度量。下面将描述这两个度量的基本组件。建议你扩展它们,以探索该模型及其所学习到的内容。

         Evaluating on the Test Dataset


        如“使用多层感知机处理姓氏分类问题”中的示例与本示例之间的训练例程没有变化一样,执行评估的代码也没有变化。总之,调用分类器的eval()方法来防止反向传播,并迭代测试数据集。与 MLP 约 50% 的性能相比,该模型的测试集性能准确率约为56%。尽管这些性能数字绝不是这些特定架构的上限,但是通过一个相对简单的CNN模型获得的改进应该足以让您在文本数据上尝试CNNs。

        Classifying or retrieving top predictions for a new surname


        在本例中,predict_nationality()函数的一部分发生了更改,如示例所示:我们没有使用视图方法重塑新创建的数据张量以添加批处理维度,而是使用PyTorch的unsqueeze()函数在批处理应该在的位置添加大小为1的维度。相同的更改反映在predict_topk_nationality()函数中。

def predict_nationality(surname, classifier, vectorizer):
    """预测一个新姓氏的国籍

    Args:
        surname (str): 要分类的姓氏
        classifier (SurnameClassifer): 分类器的实例
        vectorizer (SurnameVectorizer): 对应的向量化器
    Returns:
        包含最有可能的国籍及其概率的字典
    """
    vectorized_surname = vectorizer.vectorize(surname)  # 向量化姓氏
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(0)  # 转换为PyTorch张量
    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}  # 返回预测结果字典
def predict_topk_nationality(surname, classifier, vectorizer, k=5):
    """Predict the top K nationalities from a new surname
    
    Args:
        surname (str): the surname to classifier
        classifier (SurnameClassifer): an instance of the classifier
        vectorizer (SurnameVectorizer): the corresponding vectorizer
        k (int): the number of top nationalities to return
    Returns:
        list of dictionaries, each dictionary is a nationality and a probability
    """
    
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(dim=0)
    prediction_vector = classifier(vectorized_surname, apply_softmax=True)
    probability_values, indices = torch.topk(prediction_vector, k=k)
    
    # returned size is 1,k
    probability_values = probability_values[0].detach().numpy()
    indices = indices[0].detach().numpy()
    
    results = []
    for kth_index in range(k):
        nationality = vectorizer.nationality_vocab.lookup_index(indices[kth_index])
        probability_value = probability_values[kth_index]
        results.append({'nationality': nationality, 
                        'probability': probability_value})
    return results

new_surname = input("Enter a surname to classify: ")

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']))
        2.2.6 运行结果

        

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值