自然语言处理(nlp)前馈网络(姓氏分类的应用)

一、实验背景

       神经网络是机器学习中的一种重要模型,广泛应用于图像识别、自然语言处理等领域。感知器是最简单的神经网络,尽管它在某些任务上表现良好,但在面对复杂模式时存在局限性。为了解决这些问题,多层感知器(MLP)和卷积神经网络(CNN)应运而生。本实验旨在通过感知器、多层感知器和卷积神经网络的实现和对比,帮助大家理解神经网络的基础原理及其应用。

二、实验原理

1. 感知器与线性不可分问题

        感知器是最简单的神经网络,由输入层、权重和激活函数组成。它将输入向量与权重相乘并通过激活函数输出结果。感知器只能解决线性可分的问题,对于XOR等非线性可分问题无能为力。

感知器
XOR
XOR

       在本次研究中,我们将探索传统上称为前馈网络的神经网络模型,以及两种前馈神经网络:多层感知器(MLP)和卷积神经网络(CNN)。多层感知器在结构上扩展了我们以及学习过的的简单感知器,将多个感知器分组在一个单层,并将多个层叠加在一起。我们稍后将介绍多层感知器,并在“示例:带有多层感知器的姓氏分类”中展示它们在多层分类中的应用。

2. 多层感知器(MLP)

(1)简介

        MLP通过增加隐藏层和非线性激活函数解决了感知器的局限性。MLP不仅可以处理线性可分问题,还能处理非线性可分问题。最简单的MLP,如下图所示,由三个表示阶段和两个线性层组成。第一阶段是输入向量。这是给定给模型的向量。在“示例:对餐馆评论的情绪进行分类”中,输入向量是Yelp评论的一个收缩的one-hot表示。给定输入向量,第一个线性层计算一个隐藏向量——表示的第二阶段。隐藏向量之所以这样被调用,是因为它是位于输入和输出之间的层的输出。我们所说的“层的输出”是什么意思?理解这个的一种方法是隐藏向量中的值是组成该层的不同感知器的输出。使用这个隐藏的向量,第二个线性层计算一个输出向量。在像Yelp评论分类这样的二进制任务中,输出向量仍然可以是1。

相关原理:

 前向传播(Forward Propagation)

        前向传播是指数据从输入层经过隐藏层传递到输出层的过程。在每个隐藏层和输出层的神经元中,数据通过以下步骤进行处理:

  1. 加权求和:每个神经元接收前一层所有神经元的输出,并乘以相应的权重,然后加上一个偏置项(bias)。

 其中,wi是权重, xi是输入,b 是偏置,z 是加权求和的结果。

     2. 激活函数:将加权求和的结果通过一个非线性激活函数,得到神经元的输出。常见的激活函数有Sigmoid、ReLU和Tanh等。

反向传播(Backward Propagation):

       反向传播是指通过计算损失函数的梯度来更新模型参数(权重和偏置)的过程。其步骤如下:

  1. 计算损失:使用损失函数计算模型预测值与真实值之间的误差。常见的损失函数有均方误差(MSE)和交叉熵损失等。

     2.计算梯度:根据损失函数计算每个参数的梯度,即损失函数对每个参数的偏导数。

     3.更新参数:使用梯度下降法或其他优化算法更新参数。

激活函数:

       激活函数是MLP中的重要组成部分,它引入非线性,使得神经网络能够学习复杂的模式。以下是几种常见的激活函数:

        Sigmoid:将输入值映射到0和1之间。

         ReLU(Rectified Linear Unit):将负值映射为0,正值保持不变。

         Tanh:将输入值映射到-1和1之间。

        在下面的实验中,我们将探索传统上称为前馈网络的神经网络模型,以及两种前馈神经网络:多层感知器和卷积神经网络。多层感知器在结构上扩展了我们研究过的简单感知器,将多个感知器分组在一个单层,并将多个层叠加在一起。我们稍后将介绍多层感知器,并在“示例:带有多层感知器的姓氏分类”中展示它们在多层分类中的应用。多层感知器(MLP)是最基本的神经网络构建模块之一。它将多个感知器分组在一个单层,并将多个层叠加在一起。MLP的力量来自于添加第二个线性层和允许模型学习一个线性分割的中间表示。

(2)PyTorch中的一个实现

       MLP除了实验3中简单的感知器之外,还有一个额外的计算层,使用PyTorch的两个线性模块实例化这个想法。线性对象被命名为fc1和fc2,它们遵循一个通用约定,即将线性模块称为“完全连接层”,简称为“fc层”。除了这两个线性层外,还有一个修正的线性单元(ReLU)非线性(在实验3“激活函数”一节中介绍),它在被输入到第二个线性层之前应用于第一个线性层的输出。由于层的顺序性,必须确保层中的输出数量等于下一层的输入数量。使用两个线性层之间的非线性是必要的,因为没有它,两个线性层在数学上等价于一个线性层4,因此不能建模复杂的模式。MLP的实现只实现反向传播的前向传递。这是因为PyTorch根据模型的定义和向前传递的实现,自动计算出如何进行向后传递和梯度更新。

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

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

class MultilayerPerceptron(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
  #MultilayerPerceptron 类继承自 nn.Module,这是 PyTorch 中所有神经网络模块的基类。
#__init__ 方法初始化了 MultilayerPerceptron 类的实例。
#input_dim 是输入向量的大小。
#hidden_dim 是第一个全连接层(线性层)的输出大小。
#output_dim 是第二个全连接层的输出大小。
#super(MultilayerPerceptron, self).__init__() 调用父类 nn.Module 的初始化方法。
#self.fc1 是第一个全连接层,将输入向量映射到隐藏层。
#self.fc2 是第二个全连接层,将隐藏层的输出映射到最终的输出层     
        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):
        # x_in 是输入数据张量,其形状应为 (batch, input_dim),其中 batch 是批次大小,input_dim 是输入特征的数量。
#apply_softmax 是一个布尔标志,指示是否应用 Softmax 激活函数。如果使用交叉熵损失函数,则应设置为 False。
#intermediate = F.relu(self.fc1(x_in)):
#将输入数据通过第一个全连接层,并应用 ReLU 激活函数。
#output = self.fc2(intermediate):
#将中间层的输出通过第二个全连接层,得到最终的输出。
#if apply_softmax: output = F.softmax(output, dim=1):
#如果 apply_softmax 为 True,则对输出应用 Softmax 激活函数。
#return output 返回结果张量,其形状应为 (batch, output_dim)
        intermediate = F.relu(self.fc1(x_in))
        output = self.fc2(intermediate)

        if apply_softmax:
            output = F.softmax(output, dim=1)
        return output
    
1)实例化MLP        

        为了演示,我们使用大小为3的输入维度、大小为4的输出维度和大小为100的隐藏维度。请注意,在print语句的输出中,每个层中的单元数很好地排列在一起,以便为维度3的输入生成维度4的输出。

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)

2)Testing the MLP with random inputs
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)
#mlp 是一个多层感知器模型的实例。
#x_input 是输入数据张量,其形状应为 (batch_size, input_dim),其中 batch_size 是批次大小,input_dim 是输入特征的数量。
#apply_softmax=False 表示在前向传播过程中不应用 Softmax 激活函数。
#y_output 是模型的输出张量,其形状应为 (batch_size, output_dim),其中 output_dim 是输出类别的数量。
describe(y_output)#用于描述张量的统计信息

3)MLP with apply_softmax=True
y_output = mlp(x_input, apply_softmax=True)
describe(y_output)

3.卷积神经网络 

卷积神经网络(Convolutional Neural Networks, CNNs)是一类专门用于处理具有网格结构数据(如图像)的深度学习模型。CNNs 在图像识别、语音识别和自然语言处理等领域表现出色。以下是对卷积神经网络原理的详细分析:

   1. 基本组成部分

卷积层(Convolutional Layer):

       作用:提取输入数据的局部特征。

       操作:使用多个卷积核(filters)在输入数据上进行卷积操作,生成特征图(feature maps)。

       参数:卷积核的大小、步长(stride)、填充(padding)方式等。

激活函数(Activation Function):

       作用:引入非线性,使模型可以拟合复杂函数。

       常用激活函数:Sigmoid函数、Tanh函数、ReLU、Leaky ReLU。

Sigmoid:

S(x)=\frac{1}{1+e^{-x}}=\frac{e^{x}}{1+e^{x}}

Tanh:

tanh(x)=\tfrac{e^{x}-e^{-x}}{e^{x}+e^{-x}}

ReLU:

f(x)=max(0,x)

Leaky ReLU:

f(x)=\left\{\begin{matrix} x \: \: \: \: \: if \:x\geq 0 \\ \frac{x}{a}\: \: \: \: \: if \:x< 0 \\ \end{matrix}\right.

池化层(Pooling Layer):

      作用:降低特征图的维度,减少参数数量和计算量,同时保留主要特征。

      操作:在特征图上进行下采样(subsampling)或降采样(downsampling),如最大池化(max pooling)和平均池化(average pooling)。

      参数:池化窗口的大小、步长等。

全连接层(Fully Connected Layer):   

       作用:将前面提取到的局部特征综合起来,进行分类或回归任务。

       操作:将输入展平为一维向量,并通过多个全连接层进行处理。

2. 工作原理

卷积操作:

       卷积操作是 CNN 的核心,通过卷积核在输入数据上滑动,并进行点积运算来提取特征。卷积核的参数是通过训练学习得到的。

激活函数:

        激活函数用于引入非线性,常用的 ReLU 函数可以有效地缓解梯度消失问题,并加速训练过程。

池化操作:

       池化层用于对特征图进行降采样,常用的最大池化会取池化窗口中的最大值,从而保留主要特征并减少计算量。

全连接层:

       全连接层将前面提取到的特征综合起来,通常用于最后的分类或回归任务。通过反向传播算法,模型可以优化这些层的参数。

3. 典型结构

一个典型的卷积神经网络结构可能包含以下几个部分:

  1. 输入层:输入原始数据(如图像)。
  2. 多个卷积层和池化层交替:提取局部特征并进行降采样。
  3. 展平层(Flatten Layer):将多维特征图展平成一维向量。
  4. 多个全连接层:综合特征并进行分类或回归。
  5. 输出层:给出最终的预测结果。

       CNN通过卷积操作和池化操作捕捉输入数据的局部特征,广泛应用于图像处理和序列数据处理。CNN的卷积层通过卷积核扫描输入数据,提取特征;池化层通过降采样减少数据维度,保留重要信息。

        在本实验研究的第二种前馈神经网络,卷积神经网络,在处理数字信号时深受窗口滤波器的启发。通过这种窗口特性,卷积神经网络能够在输入中学习局部化模式,这不仅使其成为计算机视觉的主轴,而且是检测单词和句子等序列数据中的子结构的理想候选。我们在“卷积神经网络”中概述了卷积神经网络,并在“示例:使用CNN对姓氏进行分类”中演示了它们的使用。

       在本实验中,多层感知器和卷积神经网络被分组在一起,因为它们都是前馈神经网络,并且与另一类神经网络——递归神经网络(RNNs)形成对比,递归神经网络(RNNs)允许反馈(或循环),这样每次计算都可以从之前的计算中获得信息。之后,我们将介绍RNNs以及为什么允许网络结构中的循环是有益的。

        在我们介绍这些不同的模型时,需要理解事物如何工作的一个有用方法是在计算数据张量时注意它们的大小和形状。每种类型的神经网络层对它所计算的数据张量的大小和形状都有特定的影响,理解这种影响可以极大地有助于对这些模型的深入理解。

三、实验环境

  • Python 3.6.7

四、实验步骤(姓氏分类)

1.感知器的实现

(1)姓氏数据集

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

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

        SurnameDataset的实现需要实现两个函数:__getitem方法,它在给定索引时返回一个数据点;以及len方法,该方法返回数据集的长度。

class SurnameDataset(Dataset):
    # 定义一个姓氏数据集类,继承自PyTorch的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_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):
        # 从CSV文件加载数据集并创建向量化器的方法
​
        surname_df = pd.read_csv(surname_csv)
        train_surname_df = surname_df[surname_df.split=='train']
        # 加载CSV文件并提取训练集数据
​
        return cls(surname_df, SurnameVectorizer.from_dataframe(train_surname_df))
        # 创建数据集实例并返回
​
    @classmethod
    def load_dataset_and_load_vectorizer(cls, surname_csv, vectorizer_filepath):
        # 从CSV文件加载数据集并加载向量化器的方法
​
        surname_df = pd.read_csv(surname_csv)
        vectorizer = cls.load_vectorizer_only(vectorizer_filepath)
        # 加载CSV文件并加载向量化器
​
        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)
    # 创建DataLoader实例
​
    for data_dict in dataloader:
        out_data_dict = {}
        for name, tensor in data_dict.items():
            out_data_dict[name] = data_dict[name].to(device)
        # 将数据移动到指定设备
​
        yield out_data_dict
        # 生成批次数据
(2)Vocabulary, Vectorizer, and DataLoader

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

THE VOCABULARY CLASS

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

THE SURNAMEVECTORIZER

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

       虽然我们在这个示例中使用了收缩的one-hot,但是在后面的实验中,将了解其他向量化方法,它们是one-hot编码的替代方法,有时甚至更好。具体来说,在“示例:使用CNN对姓氏进行分类”中,将看到一个热门矩阵,其中每个字符都是矩阵中的一个位置,并具有自己的热门向量。

class Vocabulary(object):
    # 定义一个词汇表类

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        # 初始化方法,接受一个词到索引的映射字典,是否添加未知词标记,以及未知词标记的符号

        if token_to_idx is None:
            token_to_idx = {}
        self._token_to_idx = token_to_idx
        # 如果没有提供词到索引的映射字典,则初始化为空字典

        self._idx_to_token = {idx: token 
                              for token, idx in self._token_to_idx.items()}
        # 创建一个索引到词的映射字典

        self._add_unk = add_unk
        self._unk_token = unk_token
        # 设置是否添加未知词标记以及未知词标记的符号

        self.unk_index = -1
        if add_unk:
            self.unk_index = self.add_token(unk_token) 
        # 如果需要添加未知词标记,则将其添加到词汇表中,并记录其索引

    def to_serializable(self):
        # 将词汇表转换为可序列化的字典

        return {'token_to_idx': self._token_to_idx, 
                'add_unk': self._add_unk, 
                'unk_token': self._unk_token}

    @classmethod
    def from_serializable(cls, contents):
        # 从可序列化的字典创建词汇表实例

        return cls(**contents)

    def add_token(self, token):
        # 添加一个词到词汇表中

        try:
            index = self._token_to_idx[token]
        except KeyError:
            index = len(self._token_to_idx)
            self._token_to_idx[token] = index
            self._idx_to_token[index] = token
        # 如果词已经存在于词汇表中,则返回其索引;否则,添加新词并返回其索引

        return index
    
    def add_many(self, tokens):
        # 添加多个词到词汇表中

        return [self.add_token(token) for token in tokens]

    def lookup_token(self, token):
        # 查找词的索引

        if self.unk_index >= 0:
            return self._token_to_idx.get(token, self.unk_index)
        else:
            return self._token_to_idx[token]
        # 如果词不存在于词汇表中且设置了未知词标记,则返回未知词标记的索引;否则,抛出KeyError

    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]
        # 如果索引不存在于词汇表中,则抛出KeyError

    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):
        # 初始化方法,接受姓氏词汇表和国籍词汇表

        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
        # 遍历姓氏中的每个字符,将对应位置的向量值设为1

        return one_hot
        # 返回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)The Surname Classifier Model

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

class SurnameClassifier(nn.Module):
    # 定义一个姓氏分类器类,继承自PyTorch的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):
        # 前向传播方法,接受输入张量和是否应用softmax的标志

        intermediate_vector = F.relu(self.fc1(x_in))
        # 通过第一个全连接层并应用ReLU激活函数

        prediction_vector = self.fc2(intermediate_vector)
        # 通过第二个全连接层得到预测向量

        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)
        # 如果需要应用softmax,则对预测向量应用softmax函数

        return prediction_vector
        # 返回预测向量
(4)训练定义
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,
            # 测试损失,初始值为-1

            'test_acc': -1,
            # 测试准确率,初始值为-1

            'model_filename': args.model_state_file}
            # 模型文件名,从参数对象中获取

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:
        # 如果训练轮次大于等于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
    # 返回更新后的训练状态

def compute_accuracy(y_pred, y_target):
    # 计算准确率的方法,接受预测值和目标值

    _, y_pred_indices = y_pred.max(dim=1)
    # 获取预测值中每行的最大值索引

    n_correct = torch.eq(y_pred_indices, y_target).sum().item()
    # 计算预测正确的数量

    return n_correct / len(y_pred_indices) * 100
    # 返回准确率,单位为百分比
def set_seed_everywhere(seed, cuda):
    # 设置全局随机种子的方法,接受种子值和是否使用CUDA的标志

    np.random.seed(seed)
    # 设置NumPy的随机种子

    torch.manual_seed(seed)
    # 设置PyTorch的随机种子

    if cuda:
        torch.cuda.manual_seed_all(seed)
        # 如果使用CUDA,设置所有CUDA设备的随机种子

def handle_dirs(dirpath):
    # 处理目录路径的方法,接受目录路径

    if not os.path.exists(dirpath):
        # 如果目录路径不存在

        os.makedirs(dirpath)
        # 创建目录
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,
    # 隐藏层维度

    # 训练超参数
    seed=1337,
    # 随机种子

    num_epochs=100,
    # 训练轮数

    early_stopping_criteria=5,
    # 提前停止的标准

    learning_rate=0.001,
    # 学习率

    batch_size=64,
    # 批次大小

    # 运行时选项
    cuda=False,
    # 是否使用CUDA

    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():
    # 如果CUDA不可用

    args.cuda = False
    # 设置CUDA标志为False

args.device = torch.device("cuda" if args.cuda else "cpu")
# 设置设备为CUDA或CPU

print("Using CUDA: {}".format(args.cuda))
# 打印是否使用CUDA


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

# 处理目录
handle_dirs(args.save_dir)

(5) 数据集、模型、损失函数和优化器的实例化
if args.reload_from_files:
    # 从检查点重新训练
    print("Reloading!")
    # 打印重新加载的提示信息

    dataset = SurnameDataset.load_dataset_and_load_vectorizer(args.surname_csv,
                                                              args.vectorizer_file)
    # 加载数据集并加载向量化器

else:
    # 创建数据集和向量化器
    print("Creating fresh!")
    # 打印创建新数据集的提示信息

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

    dataset.save_vectorizer(args.vectorizer_file)
    # 保存向量化器到文件
    
vectorizer = dataset.get_vectorizer()
# 获取向量化器

classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab), 
                               hidden_dim=args.hidden_dim, 
                               output_dim=len(vectorizer.nationality_vocab))
# 创建姓氏分类器实例,设置输入维度、隐藏层维度和输出维度

handle_dirs(args.save_dir)
# 处理保存目录
(6)THE TRAINING LOOP

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

classifier = classifier.to(args.device)
# 将分类器模型移动到指定设备(CPU或GPU)

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)
# 定义Adam优化器,并设置学习率

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
        # 更新训练状态中的当前轮次索引

        # 迭代训练数据集

        # 设置:批次生成器,初始化损失和准确率为0,设置训练模式
        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)
        # 记录训练准确率

        # 迭代验证数据集

        # 设置:批次生成器,初始化损失和准确率为0,设置评估模式
        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'])

            # 步骤3. 计算损失
            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")
    # 捕捉键盘中断,打印退出信息
# 使用最佳可用模型计算测试集上的损失和准确率

classifier.load_state_dict(torch.load(train_state['model_filename']))
# 加载训练过程中保存的最佳模型状态字典

classifier = classifier.to(args.device)
# 将分类器模型移动到指定设备(CPU或GPU)

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):
    # 遍历每个批次数据

    # 计算输出
    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)

    # 计算准确率
    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']))

    (7) 模型预测
       1)对一个新的姓氏进行分类

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

def predict_nationality(name, classifier, vectorizer):  # 定义函数predict_nationality,输入参数为name, classifier和vectorizer
    vectorized_name = vectorizer.vectorize(name)  # 使用vectorizer将名字向量化
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)  # 将向量化的名字转换为张量,并调整为1行多列的格式
    result = classifier(vectorized_name, apply_softmax=True)  # 将向量化的名字输入分类器,得到结果,并应用softmax函数

    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: ")  # 提示用户输入一个姓氏,并将输入值存储在变量new_surname中
classifier = classifier.to("cpu")  # 将分类器移到CPU上进行计算
prediction = predict_nationality(new_surname, classifier, vectorizer)  # 使用predict_nationality函数预测输入姓氏的国籍
print("{} -> {} (p={:0.2f})".format(new_surname,  # 输出预测结果,包括输入姓氏、预测的国籍和概率值(保留两位小数)
                                    prediction['nationality'],
                                    prediction['probability']))

       2)检索一个新的姓氏的前K个预测结果

       从模型中获取关于新姓氏的多个预测结果,而不仅仅是最可能的一个。K表示想要的预测结果的数量。NLP中的标准实践是采用k-best预测并使用另一个模型对它们重新排序。PyTorch提供了一个torch.topk函数,它提供了一种方便的方法来获得这些预测。

vectorizer.nationality_vocab.lookup_index(8)
def predict_topk_nationality(name, classifier, vectorizer, k=5):  # 定义函数predict_topk_nationality,输入参数为name, classifier, vectorizer和k(默认为5)
    vectorized_name = vectorizer.vectorize(name)  # 使用vectorizer将名字向量化
    vectorized_name = torch.tensor(vectorized_name).view(1, -1)  # 将向量化的名字转换为张量,并调整为1行多列的格式
    prediction_vector = classifier(vectorized_name, apply_softmax=True)  # 将向量化的名字输入分类器,得到结果,并应用softmax函数
    probability_values, indices = torch.topk(prediction_vector, k=k)  # 获取结果中前k个最大概率值及其对应的索引
    
    # 返回的大小是1,k
    probability_values = probability_values.detach().numpy()[0]  # 将概率值从张量转换为NumPy数组,并取出第一个元素(1行k列)
    indices = indices.detach().numpy()[0]  # 将索引值从张量转换为NumPy数组,并取出第一个元素(1行k列)
    
    results = []  # 初始化结果列表
    for prob_value, index in zip(probability_values, indices):  # 遍历概率值和索引值
        nationality = vectorizer.nationality_vocab.lookup_index(index)  # 根据索引值查找对应的国籍
        results.append({'nationality': nationality,  # 将国籍和概率值添加到结果列表中
                        'probability': prob_value})
    
    return results  # 返回结果列表


new_surname = input("Enter a surname to classify: ")  # 提示用户输入一个姓氏,并将输入值存储在变量new_surname中
classifier = classifier.to("cpu")  # 将分类器移到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)  # 将k重置为国籍词汇表的长度
    
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)  # 使用predict_topk_nationality函数预测输入姓氏的前k个国籍

print("Top {} predictions:".format(k))  # 输出前k个预测结果
print("===================")  # 输出分隔线
for prediction in predictions:  # 遍历预测结果
    print("{} -> {} (p={:0.2f})".format(new_surname,  # 输出每个预测结果,包括输入姓氏、预测的国籍和概率值(保留两位小数)
                                        prediction['nationality'],
                                        prediction['probability']))

 (8)Dropout

        Dropout 是一种在训练神经网络时用于防止过拟合的正则化技术。它通过在每次训练过程中随机“丢弃”一部分神经元,从而减少神经元之间的相互依赖,使得模型更具泛化能力。Dropout 通常在模型的隐藏层中实现,但也可以在输入层中使用。

import torch.nn as nn  # 导入 PyTorch 神经网络模块
import torch.nn.functional as F  # 导入 PyTorch 神经网络功能模块

# 定义一个多层感知机类,继承自 nn.Module
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(F.dropout(intermediate, p=0.5))  # 在中间层应用 Dropout,并将结果通过第二层

        # 如果需要应用 softmax 激活函数,则对输出进行 softmax 处理
        if apply_softmax:
            output = F.softmax(output, dim=1)
        return output  # 返回最终输出

2.卷积神经网络实现(前面部分相关函数定义和数据与感知器一致)

(1)The CNN-based SurnameClassifier
import torch.nn as nn  # 导入 PyTorch 神经网络模块
import torch.nn.functional as F  # 导入 PyTorch 神经网络功能模块

# 定义一个姓氏分类器类,继承自 nn.Module
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),  # 输出通道数为指定通道数,卷积核大小为3
            nn.ELU(),  # 应用 ELU 激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels,  # 定义第二层卷积层,输入输出通道数均为指定通道数
                      kernel_size=3, stride=2),  # 卷积核大小为3,步幅为2
            nn.ELU(),  # 应用 ELU 激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels,  # 定义第三层卷积层,输入输出通道数均为指定通道数
                      kernel_size=3, stride=2),  # 卷积核大小为3,步幅为2
            nn.ELU(),  # 应用 ELU 激活函数
            nn.Conv1d(in_channels=num_channels, out_channels=num_channels,  # 定义第四层卷积层,输入输出通道数均为指定通道数
                      kernel_size=3),  # 卷积核大小为3
            nn.ELU()  # 应用 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)  # 将输入通过卷积网络,并在最后一维上进行 squeeze 操作
       
        prediction_vector = self.fc(features)  # 将卷积网络输出通过全连接层

        # 如果需要应用 softmax 激活函数,则对预测向量进行 softmax 处理
        if apply_softmax:
            prediction_vector = F.softmax(prediction_vector, dim=1)

        return prediction_vector  # 返回最终预测向量

(2)THE TRAINING LOOP
classifier.load_state_dict(torch.load(train_state['model_filename']))  # 加载模型的状态字典
classifier = classifier.to(args.device)  # 将模型移动到指定设备(CPU或GPU)
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)  # 设置设备(CPU或GPU)
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.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)  # 更新运行准确率

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

(3)模型预测
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}
new_surname = input("Enter a surname to classify: ")
classifier = classifier.cpu()
prediction = predict_nationality(new_surname, classifier, vectorizer)
print("{} -> {} (p={:0.2f})".format(new_surname,
                                    prediction['nationality'],
                                    prediction['probability']))

2)检索一个新的姓氏的前K个预测结果
def predict_topk_nationality(surname, classifier, vectorizer, k=5):
    # 将姓氏向量化
    vectorized_surname = vectorizer.vectorize(surname)
    vectorized_surname = torch.tensor(vectorized_surname).unsqueeze(dim=0)  # 转换为张量并添加一个维度
    prediction_vector = classifier(vectorized_surname, apply_softmax=True)  # 通过分类器进行预测,应用softmax
    probability_values, indices = torch.topk(prediction_vector, k=k)  # 获取前k个预测值和对应的索引
    
    # 返回的尺寸是1,k
    probability_values = probability_values[0].detach().numpy()  # 转换为NumPy数组
    indices = indices[0].detach().numpy()  # 转换为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? "))  # 输入要查看的前k个预测
if k > len(vectorizer.nationality_vocab):  # 如果k超过了国籍词汇表的长度
    print("Sorry! That's more than the # of nationalities we have.. defaulting you to max size :)")
    k = len(vectorizer.nationality_vocab)  # 将k设置为词汇表的最大长度
    
predictions = predict_topk_nationality(new_surname, classifier, vectorizer, k=k)  # 获取前k个预测

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

五、小结

       通过实现和对比感知器、多层感知器(MLP)和卷积神经网络(CNN),旨在帮助理解神经网络的基础原理及其应用。实验背景介绍了神经网络在机器学习中的重要性,指出感知器虽然简单,但在处理复杂模式时存在局限性,因此需要引入MLP和CNN等更复杂的模型。

       实验原理部分首先介绍了感知器及其局限性,感知器只能解决线性可分问题,对于非线性可分问题如XOR无能为力。接着,介绍了MLP,通过增加隐藏层和非线性激活函数,MLP能够处理非线性可分问题。MLP的关键在于前向传播和反向传播,通过计算损失函数的梯度来更新模型参数。最后,介绍了CNN,CNN专门用于处理具有网格结构的数据(如图像),通过卷积层提取局部特征,通过池化层降维,并通过全连接层进行分类或回归。

       实验步骤包括感知器、MLP和CNN的实现。首先,感知器的实现包括数据集的加载与预处理、向量化器和DataLoader的定义、模型的构建和训练循环的实现。在训练过程中,通过前向传播计算输出、计算损失、反向传播计算梯度并更新模型参数。接着,MLP的实现通过增加隐藏层和非线性激活函数,解决了感知器的局限性,并且在训练过程中引入了Dropout技术防止过拟合。最后,CNN的实现通过定义卷积层、池化层和全连接层,展示了CNN在处理图像和序列数据时的优势。

       实验结果表明,感知器只能处理线性可分问题,具有局限性,而MLP通过增加隐藏层和非线性激活函数,能够处理更复杂的非线性可分问题,分类准确率显著提高。CNN在处理图像和序列数据时表现出色,能够提取局部特征并进行有效分类,实验结果显示CNN在姓氏分类任务中表现良好,准确率较高。

       通过本实验,进一步理解了感知器、MLP和CNN的原理及其在实际任务中的应用。感知器适用于简单的线性可分问题,但在处理复杂数据时存在局限性。MLP通过增加隐藏层和非线性激活函数,能够处理非线性可分问题,适用于更复杂的任务。CNN在处理图像和序列数据时表现出色,能够提取局部特征并进行有效分类,适用于图像识别、语音识别和自然语言处理等领域。激活函数在MLP和CNN中引入非线性,使得模型能够学习复杂的模式,常见的激活函数包括Sigmoid、ReLU和Tanh等。Dropout是一种防止过拟合的正则化技术,通过随机“丢弃”一部分神经元,提高模型的泛化能力。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值