1、认识前馈神经网络
What is it
图1-1 前馈神经网络结构
人们大多使用多层感知机(英语:Multilayer Perceptron,缩写:MLP)作为前馈神经网络的代名词,但是除了MLP之外,卷积神经网络(英语:convolutional neural network,缩写:CNN)也是典型的前馈神经网络。
Why need it
感知机
先来简单了解一下感知机(perceptron),感知机是一种线性分类算法,通常用于二分类问题。它非常松散地模仿生物神经元,就像一条简单的神经元那样,有输入(刺激)和输出,并且用激活函数来模仿放电阈值,“信号”从输入流向输出,如图1-2所示
图1-2 感知机模型
数学上,感知机就是一个简单的线性函数:
y=f(wx+b)y=f(wx+b)y=f(wx+b)
日常问题中,通常有多个输入,用向量表示这个一般情况;即:x和w是向量,w和x的乘积替换为点积:
y=f(w⃗Tx⃗+b)y=f(\vec{w}^T\vec{x}+b)y=f(wTx+b)
激活函数用f表示,通常是一个非线性函数。
图 1-3 感知机工作原理
创建一个线性可分的数据集。数据集中的两个类绘制为圆形和星形。感知机要做的就是区分数据集上的点是圆形还是星型。感知机可以很好的处理线性可分的问题,例如经典的与(AND)或(OR)非(NOT)问题。
但是碰上非线性可分问题,例如异或(XOR)问题,感知机就束手无策了,由于没办法用线性函数分割数据,感知机模型无法收敛会导致其一直迭代。由于单层感知机不能处理异或问题,神经网络甚至一度陷入低潮。
多层感知机(MLP)
MLP是对感知机的扩展,感知机将数据向量作为输入,计算出一个输出值。在MLP中,许多感知机被分组成为各个网络层,每一层的输出是一个新的向量,而不是单个输出值。也就是说MLP把多个感知机的输出结果作为,最简单的MLP,如图4-2所示,由三个表示阶段和两个线性层组成。
一种具有两个线性层和三个表示阶段(输入向量、隐藏向量和输出向量)的MLP模型,如下图1-4所示:
图1-4 MLP可视化表示
图1-5 XOR(异或)数据集中的两个类绘制为圆形和星形。
在这个分类问题中,不存在任何一条直线可以分隔这两个类,这也就是线性不可分问题。 普通的感知机面对这种问题就捉襟见肘了,但是对多层感知机来说,这不是个事。
图1-6 从感知器(左)和MLP(右)学习的XOR问题的解决方案显示
虽然在图中显示MLP好像两个决策边界,但它实际上只是一个决策边界!决策边界就是这样出现的,因为MLP中间通过某种神奇的表示法改变了空间,使一个超平面同时出现在这两个位置上。在图1-7中,我们可以看到MLP计算的中间过程。这些点的形状表示类(星形或圆形)。我们所看到的是,MLP已经学会了“扭曲”数据所处的空间,以便在数据通过最后一层时,用一条直线来分割它们。
图1-7 MLP的输入和中间表示是可视化的。从左到右:(1)网络的输入;(2)第一个线性模块的输出;(3)第一个非线性模块的输出;(4)第二个线性模块的输出。第一个线性模块的输出将圆和星分组,而第二个线性模块的输出将数据点重新组织为线性可分的。
与之相反,如图1-8所示,感知机没有额外的一层来处理数据的形状,直到数据变成线性可分的。
图1-8 感知器的输入和输出表示。因为它没有像MLP那样的中间表示来分组和重新组织,所以它不能将圆和星分开。
How do achieve it
好了,既然我们已经知道MLP神通广大了,肯定迫不及待的想要实现它了。
2、在PyTorch中实现MLP
MLP除了由许多简单的感知器构成之外,还有一个额外的计算层。我们可以用PyTorch的两个线性模块表示。将线性模块称为“完全连接层”,简称为“fc层”。除了这两个线性层外,还有一个修正的非线性单元(ReLU),它的输入是第一个线性层的输出,它的输出作为第二个线性层的输入。在两个线性层之间加入非线性单元是必要的,因为没有它,两个线性层在数学上等价于一个线性层。基于pytorch的MLP只实现反向传播的前向传递。因为PyTorch根据模型的定义和向前传递的实现,自动计算出如何进行向后传递和梯度更新。
实例化MLP
import torch.nn as nn # 导入PyTorch中神经网络模块
import torch.nn.functional as F # 导入PyTorch中函数式接口,用于激活函数等操作
class MultilayerPerceptron(nn.Module): # 定义一个多层感知机类
def __init__(self, input_dim, hidden_dim, output_dim):
'''
初始化函数,设置MLP的层和参数。
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):
"""
前向传播函数,定义了数据如何通过网络流动。
Args:
x_in (torch.Tensor): 输入数据张量,其形状应为(batch, input_dim)
apply_softmax (bool): 是否应用softmax激活函数
如果与交叉熵损失一起使用,应设置为False
Returns:
torch.Tensor: 结果张量,其形状应为(batch, output_dim)
"""
intermediate = F.relu(self.fc1(x_in)) # 应用第一个线性层并使用ReLU激活函数
output = self.fc2(intermediate) # 应用第二个线性层
if apply_softmax: # 如果需要,应用softmax激活函数
output = F.softmax(output, dim=1) # softmax沿最后一个维度计算
return output # 返回最终的输出张量
由于MLP实现的通用性,可以为任何大小的输入建模。为了演示,我们使用大小为3的输入维度、大小为4的输出维度和大小为100的隐藏维度。
batch_size = 2 # 定义一次输入的样本数量
input_dim = 3 # 定义输入向量的维度,例如,每个样本有3个特征
hidden_dim = 100 # 定义隐藏层的神经元数量,这里是100个神经元
output_dim = 4 # 定义输出层的神经元数量,例如,对于4类分类问题
# 初始化模型
mlp = MultilayerPerceptron(input_dim, hidden_dim, output_dim)
# 使用定义好的类构造函数创建一个多层感知机实例
print(mlp) # 打印模型的结构
# 这将输出模型的层和参数信息,包括每个层的类型和参数数量
上述代码运行结果:
MultilayerPerceptron(
(fc1): Linear(in_features=3, out_features=100, bias=True)
(fc2): Linear(in_features=100, out_features=4, bias=True)
)
测试模型的连接
我们可以通过传递一些随机输入来快速测试模型的“连接”,但是因为模型还没有经过训练,所以输出是随机的。在花费时间训练模型之前,这样做是一个有用的完整性检查。
def describe(x):
"""
打印张量的类型、形状和值。
Args:
x (torch.Tensor): 需要描述的张量
"""
print("Type: {}".format(x.type())) # 打印张量的类型,例如torch.float32
print("Shape/size: {}".format(x.shape)) # 打印张量的形状,例如torch.Size([2, 3])
print("Values: \n{}".format(x)) # 打印张量的值
x_input = torch.rand(batch_size, input_dim) # 创建一个形状为(batch_size, input_dim)的随机张量
# torch.rand生成[0,1)区间内均匀分布的随机数
describe(x_input) # 使用describe函数打印x_input张量的信息
上述代码运行结果:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[0.6193, 0.7045, 0.7812],
[0.6345, 0.4476, 0.9909]])
y_output = mlp(x_input, apply_softmax=False) # 使用多层感知机模型对x_input进行前向传播
# 这里apply_softmax参数设置为False,表示不应用softmax激活函数,通常在模型训练时这样做,
# 因为PyTorch的交叉熵损失函数会内部应用softmax。
describe(y_output) # 使用describe函数打印y_output张量的信息
上述代码运行结果:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 4])
Values:
tensor([[ 0.2356, 0.0983, -0.0111, -0.0156],
[ 0.1604, 0.1586, -0.0642, 0.0010]], grad_fn=<AddmmBackward>)
读取模型的输入和输出
在前面的例子中,MLP模型的输出是一个有两行四列的张量。在某些情况下,例如在分类设置中,特征向量是一个预测向量。名称为“预测向量”表示它对应于一个概率分布。预测向量会发生什么取决于我们当前是在进行训练还是在执行推理。在训练期间,输出按原样使用,带有一个损失函数和目标类标签的表示。
但是,如果想将预测向量转换为概率,则需要额外的步骤。具体来说,需要softmax函数,它可以将输出值向量转换为概率。这个函数背后的直觉是,大的正值会导致更高的概率,小的负值会导致更小的概率。下面将apply_softmax标志设置为True:
y_output = mlp(x_input, apply_softmax=True) # 使用多层感知机模型对x_input进行前向传播
# 这里apply_softmax参数设置为True,表示在模型的输出上应用softmax激活函数,
# 这通常用于模型的预测阶段,将原始分数转换为概率分布。
describe(y_output) # 使用describe函数打印y_output张量的信息
上述代码运行结果:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 4])
Values:
tensor([[0.2915, 0.2541, 0.2277, 0.2267],
[0.2740, 0.2735, 0.2189, 0.2336]], grad_fn=<SoftmaxBackward>)
综上所述,mlp是将输入张量映射到输出张量的线性层。并且在每一对线性层之间使用非线性来打破线性关系,允许模型扭曲向量空间。在分类设置中,这种扭曲导致类之间的线性可分性。另外,可以使用softmax函数将MLP输出解释为概率。
3、MLP在姓氏分类中的应用
在本节中,我们将MLP应用于将姓氏分类到其原籍国的任务。我们首先对每个姓氏的字符进行拆分。除了数据上的差异,字符层模型在结构和实现上与基于单词的模型基本相似。我们在这个例子中引入了多类输出及其对应的损失函数。在建立了模型之后,我们完成了训练。 要先从从姓氏数据集及其预处理步骤的描述开始。然后,我们使用词汇表、向量化器和DataLoader类逐步完成从姓氏字符串到向量化小批处理的管道。
建立词汇表
#Vocabulary
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
class Vocabulary(object):
"""用于处理文本并提取词汇表进行映射的类"""
def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
"""
初始化 Vocabulary 类的一个实例。
Args:
token_to_idx (dict): 一个预先存在的标记到索引的映射字典。
add_unk (bool): 一个标志,指示是否添加未知标记(UNK token)。
unk_token (str): 要添加到词汇表中的未知标记。
"""
# 如果没有提供 token_to_idx,则创建一个空字典
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 """
# 根据序列化的内容(字典)创建 Vocabulary 类的实例
return cls(**contents)
def add_token(self, token):
"""根据标记更新映射字典。
Args:
token (str): 要添加到词汇表中的项
Returns:
index (int): 与标记对应的整数
"""
# 尝试从现有映射中获取标记的索引,如果不存在则添加标记并创建新索引
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):
"""将一系列标记添加到词汇表中
Args:
tokens (list): 字符串标记的列表
Returns:
indices (list): 与标记对应的索引列表
"""
# 对列表中的每个标记调用 add_token,并返回索引列表
return [self.add_token(token) for token in tokens]
def lookup_token(self, token):
"""检索与标记关联的索引,如果标记不存在则返回未知标记的索引。
Args:
token (str): 要查找的标记
Returns:
index (int): 与标记对应的索引
"""
# 如果已添加未知标记,则返回标记的索引或未知标记的索引
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):
"""返回与索引关联的标记
Args:
index (int): 要查找的索引
Returns:
token (str): 与索引对应的标记
Raises:
KeyError: 如果索引不在词汇表中
"""
# 如果索引在索引到标记的映射中,则返回对应的标记,否则抛出 KeyError
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):
"""返回 Vocabulary 对象的字符串表示形式"""
return "<Vocabulary(size=%d)>" % len(self)
def __len__(self):
"""返回词汇表的大小"""
return len(self._token_to_idx)
定义姓氏数据集类
姓氏数据集,它收集了来自18个不同国家的10,000个姓氏,这些姓氏是作者从互联网上不同的姓名来源收集的。该数据集将在本课程实验的几个示例中重用,并具有一些使其有趣的属性。第一个性质是它是相当不平衡的。排名前三的课程占数据的60%以上:27%是英语,21%是俄语,14%是阿拉伯语。剩下的15个民族的频率也在下降——这也是语言特有的特性。第二个特点是,在国籍和姓氏正字法(拼写)之间有一种有效和直观的关系。有些拼写变体与原籍国联系非常紧密(比如“O ‘Neill”、“Antonopoulos”、“Nagasawa”或“Zhu”)。
为了创建最终的数据集,要进行几个数据集修改操作。第一个目的是减少这种不平衡——原始数据集中70%以上是俄文,这可能是由于抽样偏差或俄文姓氏的增多。为此,我们通过选择标记为俄语的姓氏的随机子集对这个过度代表的类进行子样本。接下来,我们根据国籍对数据集进行分组,并将数据集分为三个部分:70%到训练数据集,15%到验证数据集,最后15%到测试数据集,以便跨这些部分的类标签分布具有可比性。
from torch.utils.data import Dataset # 导入PyTorch的Dataset类
import pandas as pd
class SurnameDataset(Dataset):
"""
自定义数据集类,用于处理姓氏和国籍的数据。
实现与第3.5节几乎相同(假设这是参考某个教程或文档中的一个部分)。
"""
def __init__(self, surname_df, vectorizer):
"""
Args:
surname_df (pandas.DataFrame): the dataset
vectorizer (SurnameVectorizer): vectorizer instatiated from dataset
"""
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):
"""Load dataset and make a new vectorizer from scratch
Args:
surname_csv (str): location of the dataset
Returns:
an instance of 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):
"""Load dataset and the corresponding vectorizer.
Used in the case in the vectorizer has been cached for re-use
Args:
surname_csv (str): location of the dataset
vectorizer_filepath (str): location of the saved vectorizer
Returns:
an instance of 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):
"""a static method for loading the vectorizer from file
Args:
vectorizer_filepath (str): the location of the serialized vectorizer
Returns:
an instance of SurnameVectorizer
"""
with open(vectorizer_filepath) as fp:
return SurnameVectorizer.from_serializable(json.load(fp))
def save_vectorizer(self, vectorizer_filepath):
"""saves the vectorizer to disk using json
Args:
vectorizer_filepath (str): the location to save the vectorizer
"""
with open(vectorizer_filepath, "w") as fp:
json.dump(self._vectorizer.to_serializable(), fp)
def get_vectorizer(self):
""" returns the vectorizer """
return self._vectorizer
def set_split(self, split="train"):
""" selects the splits in the dataset using a column in the dataframe """
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):
"""
根据索引index获取数据集中的一个样本。
Args:
index (int): 要获取的样本的索引
Returns:
dict: 包含一个样本的特征和标签的字典
"""
row = self._target_df.iloc[index] # 从数据集中获取第index行的数据
# 这里假设self._target_df是一个pandas DataFrame对象,存储了数据集的所有行
surname_vector = self._vectorizer.vectorize(row.surname) # 使用向量化方法将姓氏转换为数值向量
# 这里假设self._vectorizer是一个具有vectorize方法的对象,用于将文本数据转换为数值向量
nationality_index = self._vectorizer.nationality_vocab.lookup_token(row.nationality) # 将国籍转换为索引
# 这里假设self._vectorizer.nationality_vocab是一个Vocabulary对象,用于将文本标签转换为整数索引
return {'x_surname': surname_vector, # 返回一个字典,包含特征和标签
'y_nationality': nationality_index}
def get_num_batches(self, batch_size):
"""Given a batch size, return the number of batches in the dataset
Args:
batch_size (int)
Returns:
number of batches in the dataset
"""
return len(self) // batch_size
def generate_batches(dataset, batch_size, shuffle=True,
drop_last=True, device="cpu"):
"""
A generator function which wraps the PyTorch DataLoader. It will
ensure each tensor is on the write device location.
"""
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
构建词汇表向量化的向量化器
虽然词汇表将单个令牌(字符)转换为整数,但SurnameVectorizer负责应用词汇表并将姓氏转换为向量。字符串没有在空格上分割。姓氏是字符的序列,每个字符在我们的词汇表中是一个单独的标记。在“卷积神经网络”出现之前,我们将忽略序列信息,通过迭代字符串输入中的每个字符来创建输入的收缩one-hot向量表示。
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 # 在独热编码向量中对应位置设为1
return one_hot # 返回独热编码数组
@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) # 创建用于国籍的词汇表,不添加未知标记
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): 包含序列化信息的字典
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):
"""返回向量化器的序列化形式"""
return {
'surname_vocab': self.surname_vocab.to_serializable(), # 获取姓氏词汇表的序列化形式
'nationality_vocab': self.nationality_vocab.to_serializable() # 获取国籍词汇表的序列化形式
}
构造姓氏分类的MLP模型
SurnameClassifier是前面介绍的MLP的实现。第一个线性层将输入向量映射到中间向量,并对该向量应用非线性。第二线性层将中间向量映射到预测向量。
在最后一步中,可选地应用softmax操作,以确保输出和为1;这就是所谓的“概率”。它是可选的原因与我们使用的损失函数的数学公式有关——交叉熵损失。我们研究了“损失函数”中的交叉熵损失。回想一下,交叉熵损失对于多类分类是最理想的,但是在训练过程中软最大值的计算不仅浪费而且在很多情况下并不稳定。
class SurnameClassifier(nn.Module):
"""
一个用于分类姓氏的两层多层感知机(MLP)。
"""
def __init__(self, input_dim, hidden_dim, output_dim):
"""
初始化分类器,设置网络层和参数。
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): 输入数据张量,其形状应为 (batch, input_dim)。
apply_softmax (bool): 是否应用 softmax 激活函数的标志。
如果与交叉熵损失函数一起使用,应设置为 False。
Returns:
torch.Tensor: 结果张量,其形状应为 (batch, output_dim)。
"""
intermediate_vector = F.relu(self.fc1(x_in)) # 应用第一个线性层并使用 ReLU 激活函数
prediction_vector = self.fc2(intermediate_vector) # 应用第二个线性层
if apply_softmax: # 如果需要,应用 softmax 激活函数
prediction_vector = F.softmax(prediction_vector, dim=1) # 对最后一个维度应用 softmax
return prediction_vector # 返回预测结果张量
对模型进行训练吧
训练中最显著的差异与模型中输出的种类和使用的损失函数有关。在这个例子中,输出是一个多类预测向量,可以转换为概率。正如在模型描述中所描述的,这种输出的损失类型仅限于CrossEntropyLoss和NLLLoss。由于它的简化,我们使用了CrossEntropyLoss。
import numpy as np
def set_seed_everywhere(seed, cuda):
"""
设置随机种子,确保结果可重复。
Args:
seed (int): 随机种子值。
cuda (bool): 是否使用 CUDA(GPU)。
"""
np.random.seed(seed) # 设置 NumPy 的随机种子
torch.manual_seed(seed) # 设置 PyTorch 的随机种子
if cuda:
torch.cuda.manual_seed_all(seed) # 如果使用 CUDA,设置所有 GPU 的随机种子
def handle_dirs(dirpath):
"""
处理目录,如果不存在则创建。
Args:
dirpath (str): 目录的路径。
"""
if not os.path.exists(dirpath): # 检查目录是否存在
os.makedirs(dirpath) # 如果目录不存在,则创建目录
from argparse import Namespace
import os
import torch
# 创建一个Namespace对象,用于存储命令行参数解析的结果
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(GPU)进行训练
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 # 设置args中的cuda选项为False
# 设置设备,根据是否使用CUDA决定使用CPU或GPU
args.device = torch.device("cuda" if args.cuda else "cpu")
# 打印是否使用CUDA的信息
print("Using CUDA: {}".format(args.cuda))
# 为了可重复性,设置随机种子
set_seed_everywhere(args.seed, args.cuda)
# 处理目录,确保保存目录存在
handle_dirs(args.save_dir)
上述代码运行结果(不同设备的结果不一致):
Expanded filepaths:
model_storage/ch4/surname_mlp/vectorizer.json
model_storage/ch4/surname_mlp/model.pth
Using CUDA: False
# 使用 SurnameDataset 类的类方法 load_dataset_and_make_vectorizer 加载数据集
# 并创建一个向量化器,传入 args 中指定的 CSV 文件路径
dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
# 从 dataset 对象获取向量化器
vectorizer = dataset.get_vectorizer()
# 初始化 SurnameClassifier 类的实例,即我们的模型
# input_dim 设置为向量化后的姓氏特征的维度,即姓氏词汇表的大小
# hidden_dim 设置为 args 中定义的隐藏层维度
# output_dim 设置为向量化后的国籍特征的维度,即国籍词汇表的大小
classifier = SurnameClassifier(input_dim=len(vectorizer.surname_vocab),
hidden_dim=args.hidden_dim,
output_dim=len(vectorizer.nationality_vocab))
# 将模型 classifier 移动到 args 中指定的设备上,这通常是一个 GPU 或 CPU
classifier = classifier.to(args.device)
# 初始化损失函数,这里使用 PyTorch 的 CrossEntropyLoss
# dataset.class_weights 可能是一个由数据集类计算的权重,用于处理类别不平衡问题
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
# 初始化优化器,这里使用 PyTorch 的 Adam 算法
# 传入模型的参数 classifier.parameters() 和 args 中定义的学习率
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
# 如果设置了从文件重新加载
if args.reload_from_files:
# 打印信息,表明正在重新加载
print("Reloading!")
# 从指定的CSV和向量化器文件中加载数据集和向量化器
dataset = SurnameDataset.load_dataset_and_load_vectorizer(
args.surname_csv, # 指定的CSV文件路径
args.vectorizer_file # 指定的向量化器文件路径
)
else:
# 如果没有设置从文件重新加载
# 打印信息,表明正在从头开始创建
print("Creating fresh!")
# 从指定的CSV文件中加载数据集并创建新的向量化器
dataset = SurnameDataset.load_dataset_and_make_vectorizer(args.surname_csv)
# 保存新创建的向量化器到文件
dataset.save_vectorizer(args.vectorizer_file)
# 获取数据集的向量化器
vectorizer = dataset.get_vectorizer()
# 根据向量化器中的信息初始化分类器
# 其中,input_dim 是姓氏词汇表的大小,output_dim 是国籍词汇表的大小
classifier = SurnameClassifier(
input_dim=len(vectorizer.surname_vocab), # 姓氏词汇表的大小
hidden_dim=args.hidden_dim, # 隐藏层维度
output_dim=len(vectorizer.nationality_vocab) # 国籍词汇表的大小
)
# 将模型和类别权重移动到指定的设备(CPU或GPU)
classifier = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)
# 初始化损失函数,使用CrossEntropyLoss,并传入类别权重
loss_func = nn.CrossEntropyLoss(dataset.class_weights)
# 初始化优化器,使用Adam算法
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
# 初始化学习率调度器,使用ReduceLROnPlateau根据验证损失减少学习率
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() # 将损失移动到CPU并获取数值
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进度条
epoch_bar.update()
except KeyboardInterrupt:
# 如果用户中断训练(例如使用Ctrl+C),打印退出信息
print("Exiting loop")
检查训练结果
# 使用训练过程中保存的最佳模型状态更新分类器
classifier.load_state_dict(torch.load(train_state['model_filename']))
# 将分类器和类别权重移动到指定的设备(CPU或GPU)
classifier = classifier.to(args.device)
dataset.class_weights = dataset.class_weights.to(args.device)
# 初始化损失函数,使用带有类别权重的CrossEntropyLoss
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']))
输入自己的姓氏看看分类器表现如何吧
def predict_topk_nationality(name, classifier, vectorizer, k=5):
"""
预测名称的前k个最可能的国籍及其概率。
Args:
name (str): 要预测的名称。
classifier (SurnameClassifier): 分类器实例。
vectorizer (SurnameVectorizer): 向量化器实例。
k (int): 要返回的前k个最可能的国籍。
Returns:
list: 包含最有可能的国籍和其概率的字典列表。
"""
# 将名称向量化
vectorized_name = vectorizer.vectorize(name)
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)
# 将概率值和索引从PyTorch张量转换为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
# 请求用户输入一个要分类的姓氏
new_surname = input("Enter a surname to classify: ")
# 将分类器移动到CPU设备
classifier = classifier.to("cpu")
# 请求用户输入想要看到的前k个预测结果的数量
k = int(input("How many of the top predictions to see? "))
# 如果用户请求的k大于国籍的数量,则调整k的值
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)
# 使用predict_topk_nationality函数获取前k个预测结果
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']))
Enter a surname to classify: Chang
How many of the top predictions to see? 3
上述代码运行结果:
Top 3 predictions:
===================
Chang -> Korean (p=0.40)
Chang -> Chinese (p=0.29)
Chang -> Irish (p=0.15)
如何系统的去学习大模型LLM ?
作为一名热心肠的互联网老兵,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。
但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的 AI大模型资料
包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
😝有需要的小伙伴,可以V扫描下方二维码免费领取🆓
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。
四、AI大模型商业化落地方案
阶段1:AI大模型时代的基础理解
- 目标:了解AI大模型的基本概念、发展历程和核心原理。
- 内容:
- L1.1 人工智能简述与大模型起源
- L1.2 大模型与通用人工智能
- L1.3 GPT模型的发展历程
- L1.4 模型工程
- L1.4.1 知识大模型
- L1.4.2 生产大模型
- L1.4.3 模型工程方法论
- L1.4.4 模型工程实践
- L1.5 GPT应用案例
阶段2:AI大模型API应用开发工程
- 目标:掌握AI大模型API的使用和开发,以及相关的编程技能。
- 内容:
- L2.1 API接口
- L2.1.1 OpenAI API接口
- L2.1.2 Python接口接入
- L2.1.3 BOT工具类框架
- L2.1.4 代码示例
- L2.2 Prompt框架
- L2.2.1 什么是Prompt
- L2.2.2 Prompt框架应用现状
- L2.2.3 基于GPTAS的Prompt框架
- L2.2.4 Prompt框架与Thought
- L2.2.5 Prompt框架与提示词
- L2.3 流水线工程
- L2.3.1 流水线工程的概念
- L2.3.2 流水线工程的优点
- L2.3.3 流水线工程的应用
- L2.4 总结与展望
阶段3:AI大模型应用架构实践
- 目标:深入理解AI大模型的应用架构,并能够进行私有化部署。
- 内容:
- L3.1 Agent模型框架
- L3.1.1 Agent模型框架的设计理念
- L3.1.2 Agent模型框架的核心组件
- L3.1.3 Agent模型框架的实现细节
- L3.2 MetaGPT
- L3.2.1 MetaGPT的基本概念
- L3.2.2 MetaGPT的工作原理
- L3.2.3 MetaGPT的应用场景
- L3.3 ChatGLM
- L3.3.1 ChatGLM的特点
- L3.3.2 ChatGLM的开发环境
- L3.3.3 ChatGLM的使用示例
- L3.4 LLAMA
- L3.4.1 LLAMA的特点
- L3.4.2 LLAMA的开发环境
- L3.4.3 LLAMA的使用示例
- L3.5 其他大模型介绍
阶段4:AI大模型私有化部署
- 目标:掌握多种AI大模型的私有化部署,包括多模态和特定领域模型。
- 内容:
- L4.1 模型私有化部署概述
- L4.2 模型私有化部署的关键技术
- L4.3 模型私有化部署的实施步骤
- L4.4 模型私有化部署的应用场景
学习计划:
- 阶段1:1-2个月,建立AI大模型的基础知识体系。
- 阶段2:2-3个月,专注于API应用开发能力的提升。
- 阶段3:3-4个月,深入实践AI大模型的应用架构和私有化部署。
- 阶段4:4-5个月,专注于高级模型的应用和部署。
这份完整版的大模型 LLM 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
😝有需要的小伙伴,可以Vx扫描下方二维码免费领取🆓