文章目录
1.项目简介
本项目的功能如下:
即输入姓,输出最有可能的国家名 或 最有可能的K个国家名。
本文的重点为如何构建这个CNN网络模型。
接下来,我将开始我的叙述。
2.前置知识
本部分将详细介绍本项目所需的两个核心概念,即多层感知机和卷积神经网络,有相关基础者可自行跳过。
其他机器学习基础知识例如损失函数、优化器等,我将放在下一节具体实现部分进行讲解。
另外,本部分为笔者自己的理解,若有错误,欢迎大家指出和交流。
在介绍本部分时,我将按照what,why的逻辑展开。
即:它是什么;为什么要使用它;
至于how部分,即如何使用它,我将在下一节结合具体代码进行介绍。
本部分目录如下:
- 2.1 单层感知机
- 2.2 多层感知机
- 2.3 卷积操作
- 2.4 卷积神经网络
2.1 单层感知机
what
单层感知机是机器学习中最为基础的方法之一,也可以认为是一种最为简单的神经网络,其模型结构与逻辑回归是一致的,都是多个输入,乘以权值求和再加上偏置,再经过激活函数得到输出,如下图所示。
我们可以认为其为一个线性函数:
S
=
∑
i
=
1
n
x
i
w
i
S=\sum_{i=1}^{n} x_iw_i
S=i=1∑nxiwi
若我们令S=0,即可构造一条直线:
∑
i
=
1
n
x
i
w
i
=
0
\sum_{i=1}^{n} x_iw_i=0
i=1∑nxiwi=0
利用这条直线,我们即可实现简单的二分类,如下图所示:
同时,我们可以发现,最终的输出并非S,而是S经过激活函数的结果。
那为什么要经过激活函数?我认为是受到了神经细胞的启发,即神经细胞只有收到的刺激足够大,才能产生兴奋。
但其实激活函数的最关键的作用是在于其非线性性质,这个将在下节多层感知机进行叙述。
why:
从上述描述可知。单层感知机通过对输入数据进行线性加权和阈值处理,可以实现简单的分类功能。
同时,其也引入了神经网络的概念。
故其虽然简单,且分类性能不强,但却是人工神经网络的重要基础模型之一。
2.2 多层感知机
what:
首先我们来看多层感知机的形象化表示:
(一般来说,其由输入层,隐层,输出层组成。隐层的数目任意。)
本图中激活函数未标出
通过观察,我们可以发现,其本质上就是多个单层感知机串联起来。
那么这么串联起来会有什么好处呢?以下面经典问题(异或)为例:
对于该问题,很明显,这是一个线性不可分问题,故通过单层感知机无法将不同类别的点分开。
但是,多层感知机却可以!!!为什么呢?请读者先思考,我将在why部分解答。
why:
其实上述问题的关键在于激活函数,试想一下,若没有激活函数,那么两层的感知机可表示为:
F
(
X
)
=
W
2
∗
(
W
1
∗
X
)
F(X)=W_2*(W_1*X)
F(X)=W2∗(W1∗X)
但此时,我们应该能找到一个
W
3
W_3
W3使得:
F
(
X
)
=
W
3
∗
X
=
W
2
∗
(
W
1
∗
X
)
F(X)=W_3*X=W_2*(W_1*X)
F(X)=W3∗X=W2∗(W1∗X)
故即其本质上还是一个线性函数,还是个单层感知机。
是不是能体会到激活函数的作用了?
话说回来:若加上了激活函数:
F
(
X
)
=
σ
(
W
2
∗
(
σ
(
W
1
∗
X
)
)
)
F(X)=\sigma(W_2*(\sigma(W_1*X)))
F(X)=σ(W2∗(σ(W1∗X)))
是不是就找不到
W
3
W_3
W3了?
函数非线性性是不是增强了?
依据通用近似原理,只要隐层神经元个数足够多,那么可以拟合任何一个问题。小小的异或问题就更不用说了。
2.3 卷积操作
what:
卷积操作即利用卷积核进行运算。
具体的来说,卷积核做的是线性运算,核上的每个值与它滑动到的对应位置上的值相乘,然后把这些值相加。
对了,需要注意的是,人们常说的几维卷积的意思并非原始数据的维度,而是卷积核可以移动的维度!!
下图是一个二维卷积的例子:
why:
至于卷积的作用,我想有很多。例如在数字图像处理中,卷积可以进行去噪,提取特征等,例如使用(-1,1)卷积核,即可提取图像中的纵向纹路。
基于此,我们认为:卷积可以用于从输入数据中提取特征。
2.4 卷积神经网络
what:
卷积神经网络,顾名思义,即采用卷积操作提取特征的网络。其大致结构如下图所示:
是不是与之前说的多层感知机很类似?只不过多层感知机利用权值矩阵W产生下一层的数据,而卷积神经网络中采用卷积操作产生。
所以在我看来,卷积神经网络是多层感知机的变种。并且,卷积神经网络依据卷积操作的特性,引入了池化等操作。
又回到熟悉的质疑?我们为什么要用卷积核提取特征呢?为什么不用多层感知机?
why:
对于上面的回答我觉得还是得具体问题具体分析,肯定是多层感知机有一些缺陷才导致了卷积神经网络的出现。我认为主要原因有两个:
1.局部感知: 对于具有局部相关性的数据,例如相邻的像素、声音片段或单词。他们之间存在相关性。卷积操作可以通过在局部区域应用滤波器(也称为卷积核)来捕获这种局部相关性,从而有效地提取局部特征。这种局部感知的特性使得卷积神经网络在处理具有空间结构的数据时非常有效。
2. 参数共享: 卷积操作具有参数共享的特性,即在卷积核的滑动过程中,同一个滤波器的参数被应用于输入的不同位置。这样可以大大减少模型的参数数量,降低了过拟合的风险,并且使得模型更加轻量化,便于训练和部署。
好了,我认为核心要点我们已经大致掌握了,下面进行实践阶段,即利用卷积神经网络进行姓-国家名预测。
3.项目实现
首先,我们对本项目进行一个分析,确定底层逻辑:
我们认为:本项目是一个有监督,多分类问题。
1. 有监督:本项目的标签为——国家名
2. 多分类:首先,输出的结果为国家名,离散值,故为分类问题而非回归问题; 其次,国家名数目大于2,故为多分类问题。
本部分的行文逻辑将按照以下思路进行:
目录如下:
- 3.1 文本表示(数据集展示+处理)
- 3.2 分类模型(MLP模型+CNN网络)
- 3.3 损失函数
- 3.4 优化算法
- 3.5 流程组装
3.1 文本表示(数据集展示+处理)
首先我们来观察一下数据集,部分数据集截图如下所示:
可观察到其由两列组成,即姓(surname),国家名(nationality)。很显然,无法直接利用该数据集,故需要对其进行预处理。
首先,确定Y(nationality)类别
print(len(vectorizer.nationality_vocab))
结果为:18,即共有18类。
然后对X(surname)进行向量化处理:
class SurnameDataset(Dataset):
def __getitem__(self, index):
row = self._target_df.iloc[index]
surname_matrix = \
self._vectorizer.vectorize(row.surname, self._max_seq_length)
nationality_index = \
self._vectorizer.nationality_vocab.lookup_token(row.nationality)
return {'x_surname': surname_matrix,
'y_nationality': nationality_index}
class SurnameVectorizer(object):
"""向量化"""
def vectorize(self, surname):
"""
输出姓的one-hot编码
"""
one_hot_matrix_size = (len(self.character_vocab), self.max_surname_length)
one_hot_matrix = np.zeros(one_hot_matrix_size, dtype=np.float32)
for position_index, character in enumerate(surname):
character_index = self.character_vocab.lookup_token(character)
one_hot_matrix[character_index][position_index] = 1
return one_hot_matrix
def from_dataframe(cls, surname_df):
"""
生成词向量
"""
character_vocab = Vocabulary(unk_token="@")
nationality_vocab = Vocabulary(add_unk=False)
max_surname_length = 0
for index, row in surname_df.iterrows():
max_surname_length = max(max_surname_length, len(row.surname))
for letter in row.surname:
character_vocab.add_token(letter)
nationality_vocab.add_token(row.nationality)
return cls(character_vocab, nationality_vocab, max_surname_length)
我们可以看到,经过处理后,输入词变为张量:
其形状为77*17.
3.2 分类模型(MLP模型+CNN网络)
3.2.1 MLP模型
首先是网络结构定义部分:
# 分类器
# 2个全连接层(输入-隐,隐-输出)
class SurnameClassifier(nn.Module):
""" 两层分类任务 """
def __init__(self, input_dim, hidden_dim, output_dim):
"""
Args:
input_dim (int): the size of the input vectors
hidden_dim (int): the output size of the first Linear layer
output_dim (int): the output size of the second Linear layer
"""
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):
"""
前向传播,过程大概如下:
输入->全连接层1->relu->全连接层2->是否进行softmax
"""
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
定义的网络结构为:输入层-隐层-输出层
然后在各层之间加上激活函数,从而提高该网络的非线性性和分类性能。
激活函数
常见的激活函数,例如relu,tanh等,其图像如下所示:
在pytorch中,直接调用:
y=F.relu(x)
即可在x和y之间加上了激活函数。
3.2.1 CNN网络
首先是网络结构定义部分:
class SurnameClassifier(nn.Module):
def __init__(self, initial_num_channels, num_classes, num_channels):
"""
卷积网络
"""
super(SurnameClassifier, self).__init__()
# 卷积块
self.convnet = nn.Sequential(
nn.Conv1d(in_channels=initial_num_channels,
out_channels=num_channels, kernel_size=3),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3, stride=2),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3, stride=2),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3),
nn.ELU()
)
self.fc = nn.Linear(num_channels, num_classes)
def forward(self, x_surname, apply_softmax=False):
# print(x_surname)
features = self.convnet(x_surname).squeeze(dim=2)
prediction_vector = self.fc(features)
if apply_softmax:
prediction_vector = F.softmax(prediction_vector, dim=1)
return prediction_vector
我们的定义的卷积神经网络由两部分组成,卷积块和预测块
卷积块:
self.convnet = nn.Sequential(
nn.Conv1d(in_channels=initial_num_channels,
out_channels=num_channels, kernel_size=3),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3, stride=2),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3, stride=2),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3),
nn.ELU()
)
卷积块的结构如下:
- 一维卷积:卷积核大小为3,步长为1
- 激活函数ELU
- 一维卷积:卷积核大小为3,步长为2
- 激活函数ELU
- 一维卷积:卷积核大小为3,步长为2
- 激活函数ELU
- 一维卷积:卷积核大小为3,步长为1
- 激活函数ELU
预测块:
预测块由一个全连接层组成
self.fc = nn.Linear(num_channels, num_classes)
输出各类别的得分,若经过softmax函数,即可得到各类别的概率。
为什么说能得到各类别的概率,我们来看一下softmax的公式:
s
o
f
t
m
a
x
(
X
)
=
e
x
i
∑
i
=
1
n
e
x
i
softmax(X)=\frac{e^{x_i}}{\sum_{i=1}^{n}e^{x_i}}
softmax(X)=∑i=1nexiexi
我们可发现,各类别之和为均大于0,且之和为1。这是否与概率的概念一致?
激活函数:ELU
E
L
U
(
x
)
=
{
x
,
if
x
>0
α
∗
(
e
x
p
(
x
)
−
1
)
,
otherwise
ELU(x) = \begin{cases} x, & \text{if $x$ >0} \\ \alpha*(exp(x)-1), & \text{otherwise} \\ \end{cases}
ELU(x)={x,α∗(exp(x)−1),if x >0otherwise
其图像如下所示:
3.3 损失函数
输入的数据经过了分类模型后,即可得到一个预测结果。
此时我们要计算预测结果与实际结果之间的差距:故引入了损失函数。
本例中我们使用的损失函数为交叉熵损失函数——CrossEntropyLoss。
在介绍该损失函数之前,我们要先了解几个概念:
3.3.1 信息熵
信息熵为离散随机事件的出现概率。
一个系统越是有序,信息熵就越低;反之,一个系统越是混乱,信息熵就越高。
所以说,信息熵可以被认为是系统有序化程度的一个度量。
其计算公式如下:
3.3.2 交叉熵损失
交叉熵损失源于信息论中的熵概念,用于衡量两个概率分布之间的差异。在机器学习和深度学习中,它用来量化模型预测的概率分布与真实标签分布之间的差距。
其公式如下:
下面我将举例介绍:计算一个三分类任务的损失函数。
首先真实标签的分布,我们假设为[0,0,1],即真实值为第三类;
然后是预测分布,我们回想一下上一节的softmax函数,各类别得分经过softmax函数后,可得到各类别的概率,这即为预测分布。假设预测分布为[0.2,0.3,0.5],那么最后的损失函数为:H(P,Q)=-log(0.5)
在pytorch中,直接调用交叉熵损失函数即可:
nn.CrossEntropyLoss(weight=dataset.class_weights)
3.4 优化算法
现在我们知道了分类模型的预测损失,那么现在我们就要思考如何减少该损失?
如何减少该损失——更新参数(在卷积神经网络中,即为更新卷积核)
最理想的方法为直接求得最优点,但在实际情况中,我们很难直接求出该点,故此时我们思考另一种更新卷积核的方法——梯度下降算法。
何为梯度下降算法:
梯度下降可以理解为你站在山的某处,想要下山,此时最快的下山方式就是你环顾四周,哪里最陡峭,朝哪里下山,一直执行这个策略,在第N个循环后,你就到达了山的最低处。
那么如何找到下山的方向?回想一下数学中求导的概念,我将以一元函数为例:
y
=
f
(
x
)
y=f(x)
y=f(x)
对其进行求导:
∇
y
=
∂
y
∂
x
\nabla{y}=\frac{∂y}{∂x}
∇y=∂x∂y
更新x:
x
=
x
−
a
∗
∇
y
x=x-a*\nabla{y}
x=x−a∗∇y
- a为学习率
- ∇ y \nabla{y} ∇y为梯度
其更新过程如下图所示:
好了,最核心的更新思想我们已经掌握了。本例中我们使用的优化算法为Adam。那么我们为什么使用它?它好在哪?
思考以下两种情况:
(1)在平原上找最低点,因为求得的梯度几乎为0,故若以梯度下降算法更新,需迭代非常多次。
(2)在峡谷中,梯度非常大,若此时学习率a很大,则会一直震荡,收敛速度也很慢。可见下图的例子:
对于该问题,Adam算法即可解决(我认为其为动量法和自适应梯度法的结合)
Adam算法即自适应时刻估计方法(Adaptive Moment Estimation),能计算每个参数的自适应学习率。这个方法不仅存储了AdaDelta先前平方梯度的指数衰减平均值,而且保持了先前梯度M(t)的指数衰减平均值,这一点与动量类似。
该算法的具体过程如下所示:
在pytorch中,Adam算法可直接调用函数:
optimizer = optim.Adam(classifier.parameters(), lr=args.learning_rate)
3.5 流程组装
至此,我们基本了解了本项目各步骤的核心内容,本部分我将总结本项目的流程,以及各流程我们需要做的事,并将之前介绍的内容串联起来。
1.数据预处理
- 文本向量化
- 划分训练集、验证集、测试集
2.构造卷积神经网络模型
- 定义网络结构
- 定义损失函数
- 定义优化算法
3.训练模型
- 利用优化算法更新卷积核参数
4.模型评价
- 在测试集上验证模型性能,选择正确率等参数。
4.项目效果
4.1 输出最有可能的国家名
4.2 输出K个最有可能的国家名
5.小结
本代码是读者NLP课程中的一个实验。
在本实验中,我们探讨了从感知机到多层感知机(MLP),再到卷积神经网络(CNN)在处理姓氏分类任务上的应用和实现。
感知机作为最简单的神经网络模型,在线性可分任务上表现良好,但在处理更复杂的数据模式时存在一定局限性。
MLP通过增加隐藏层和非线性激活函数,显著提升了模型的表达能力,从而可以解决更为复杂的分类问题。但其也存在类似参数过多,无法充分利用数据中的空间信息等缺陷。
CNN通过利用卷积层来捕捉数据中的局部特征,大幅提升了模型在处理图像和视频等任务中的性能表现。
在本研究的示例中,我们展示了如何利用CNN来处理姓氏分类任务,通过卷积操作捕捉姓氏中局部字符序列的特征,从而提高了分类精度。
通过比较MLP和CNN,我们可以看出不同模型在处理不同类型数据时的优势和劣势。MLP适用于较为简单和结构化的数据,而CNN在处理具有空间结构的数据时表现更为出色。这为我们在实际应用中选择合适的模型提供了重要的参考。