2023年了nlp还存不存在我不知道,数学家的思想真的有意思。
前文介绍了线性回归分类器和softmax分类器,并证明了softmax分类器是由线性回归分类器并联而成的。本文将介绍以LR模型为基础的另一个分类器:神经网络。我们将介绍神经网络模型中的一些核心概念和训练过程,并介绍神经网络模型与LR模型及softmax模型的关系。可以帮助机器学习领域的初学者对神经网络模型建立一个基本的认知。
1. 神经元模型
下图展示了一个神经元模型的示意图。神经元的定义式为:
a
=
σ
(
∑
i
w
i
∗
x
i
+
b
)
a = \sigma(\sum_i w_i * x_i + b)
a=σ(i∑wi∗xi+b)
也可以写成矩阵式:
a
=
σ
(
w
T
∗
x
+
b
)
a = \sigma(w^T * x + b)
a=σ(wT∗x+b)
其中
x
x
x是输入,
a
a
a是输出,
σ
\sigma
σ称为激活函数,
w
w
w是神经元每个输入的权重,
b
b
b为偏移量。
可以看到,如果我们把 σ \sigma σ定义为 s i g m o i d sigmoid sigmoid函数,那么这个神经元模型就是一个标准的LR模型。
2. 单层神经网络
我们使用多个神经元模型并联起来,我们得到一个单层神经网络:
如上图所示,这是一个包含
n
n
n个输入和
m
m
m个输出的单层网络,一共包含
m
(
n
+
1
)
m(n+1)
m(n+1)个权重参数。我们可以把所有权重表示为一个n行m列的矩阵
W
W
W,通过矩阵
W
W
W表示单层神经网络的权重。该网络可以表示为:
a
=
σ
(
W
T
x
+
b
)
a = \sigma (W^Tx + b)
a=σ(WTx+b)
如果我们把
σ
\sigma
σ定义为
s
i
g
m
o
i
d
sigmoid
sigmoid函数,则上边的这个网络,等效于一个softmax分类器。下图为softmax分类器的示意图:
由softmax分类器的性质可知,单层神经网络只能处理线性可分的数据空间。对于线性不可分的数据空间,需要引入隐藏层。
3. 包含隐藏层的神经网络
包含隐藏层的神经网络可以对非线性可分的数据空间进行分类。下图表示了一个包含一个隐藏层的神经网络,每个层内部节点没有关联,层之间的节点互相全连接。这些层也称为全连接层。
清注意,每个全连接层输出之前,都要经过激活函数
σ
\sigma
σ。这个
σ
\sigma
σ函数必须是一个非线性函数。为什么是非线性函数呢?因为容易证明,如果
σ
\sigma
σ是线性函数,两个全连接层将退化为一个全连接层。因为存在
W
W
W使得
W
T
x
=
W
1
T
W
2
T
x
W^Tx=W_1^TW_2^Tx
WTx=W1TW2Tx
4. 激活函数
非线性激活函数的存在,使得多层神经网络(或者叫深度神经网络)可以从数据中学到一些高维度的特征。激活函数一般有以下几种选择:
4.1 s i g m o i d sigmoid sigmoid函数
下边是
s
i
g
m
o
i
d
sigmoid
sigmoid函数的表达式及图像:
σ
(
x
)
=
1
1
+
exp
(
−
x
)
\sigma(x)=\frac{1}{ 1 + \exp(-x)}
σ(x)=1+exp(−x)1
s
i
g
m
o
i
d
sigmoid
sigmoid函数可以把任何实数压缩到0到1之间并且实现平缓过度。
4.2 t a n h tanh tanh函数
下边是
t
a
n
h
tanh
tanh函数的表达式及图像:
σ
(
x
)
=
exp
(
x
)
−
exp
(
−
x
)
exp
(
x
)
+
exp
(
−
x
)
\sigma(x)=\frac{\exp(x) - \exp(-x)}{ \exp(x) + \exp(-x)}
σ(x)=exp(x)+exp(−x)exp(x)−exp(−x)
t
a
n
h
tanh
tanh输出结果的范围比
s
i
g
m
o
i
d
sigmoid
sigmoid更大一点,从-1到1。并且也实现了平滑过渡。
上边这两个函数因为存在平滑过渡的区间,因此求导的时候速度会比较慢,因此引入一个更简单的版本。
4.3 h a r d t a n h hard\ tanh hard tanh函数
h
a
r
d
t
a
n
h
hard\ tanh
hard tanh表达式:
σ
(
x
)
=
{
−
1
x
<
−
1
x
−
1
≤
x
≤
1
1
x
>
1
\sigma(x)=\begin{cases} -1 & x < -1 \\ x & -1 \le x \le 1\\ 1 & x > 1 \end{cases}
σ(x)=⎩
⎨
⎧−1x1x<−1−1≤x≤1x>1
h
a
r
d
t
a
n
h
hard\ tanh
hard tanh图像
4.4 r e l u relu relu函数
目前最流行的应该就是这个
r
e
l
u
relu
relu函数了。这个函数最简单,求导速度最快。
r
e
l
u
relu
relu函数表达式:
σ
(
x
)
=
max
(
0
,
1
)
\sigma(x)=\max(0,1)
σ(x)=max(0,1)
r
e
l
u
relu
relu函数图像:
确实是非常简单残暴的激活函数。
5. 神经网络模型的训练
5.1 神经网络的计算图
我们上边介绍了一些比较基础的神经网络模型,实际上的模型可能会更加复杂,无论这些模型多么复杂,我们都可以把他们用一个有向无环图来表示,并把他们的计算过程转化为对图的遍历过程。我们从上文中介绍的单层神经网络入手来说明如何把神经网络模型转化为图。我们用
x
x
x表示输入特征值向量,
W
W
W表示输出层权重矩阵,
b
b
b表示输出层偏移量向量,
σ
\sigma
σ表示激活函数,
a
a
a表示输出结果向量。该模型的输出可以用公式表示为:
a
=
σ
(
z
)
z
=
y
+
b
y
=
W
T
x
a=\sigma(z)\\ z=y + b\\ y= W^Tx
a=σ(z)z=y+by=WTx
上边这个公式可以表示为如下图:
可以看到上图包含三个计算节点(以方格表示),和若干个数据节点。有些数据节点不依赖于任何前提,例如: x x x, W W W, b b b。这三个节点在计算开始时就已经存在,有些数据节点依赖于其他节点,例如 y y y, z z z, a a a等,必须等待前边被依赖的计算节点完成计算之后该节点才能激活。上图中的节点可以根据依赖和被依赖关系进行排列,被依赖节点必须排在依赖节点的前面,这样的顺序就叫拓扑顺序。
5.2 前向传播和后向传播
我们根据节点的拓扑顺序(或者说顺着上图的箭头方向),逐步把参数放入计算单元,可以计算出下一个计算单元所需的参数。我们可以依次遍历上图所有计算单元并最终得到结果输出
a
a
a。这个过程就叫前向传播(Forward Propergation)。前向传播的主要作用就是进行预测,
a
a
a就是预测的结果。
对于一个已经打了标记的训练数据样本,我们可以将一个模型的预测结果和它的标记进行对比,并将预测和标记之间的差进行梯度计算,从节点
a
a
a反向遍历全图,反推出每一个参数的梯度,并最终用于更新我们的模型参数
W
W
W和
b
b
b。这个过程就叫后向传播(Backward Propergation)。关于后向传播的原理,我们将在下一篇文章进行详细介绍。
5.3 神经网络的训练步骤
接下来进入本文最重磅的部分,如何计算神经网络的梯度。首先我们必须说明一下一个给定的神经网络模型的梯度计算的过程:
- 初始话神经网络参数 θ 0 \theta_0 θ0。
- 从训练集获取一个样本,进行一次前向传播。所谓前向传播,就是执行一次预测操作。
- 预测结果与样本标签进行比对,然后进行一次后向传播,并计算各个参数的梯度值。
- 使用梯度值更新模型各个参数。然后从2开始进入下一个循环,直到达到设定的训练条件。
这里有几个与训练循环有关的数值名称需要我们特别注意一下:
名称 | 解释 |
---|---|
batch | 一般情况下整个训练集非常大,一次性把整个训练集丢进优化器进行优化可能会占用太大的资源,因此可以把整个训练集分成若干份小训练数据集,每一份就称为一个batch。 |
iteration | 每个batch中所有样本完成一次前向传播和后向传播,并更新参数值,就称为一个iteration。 |
epoch | 把整个训练数据集跑一个iteration,就称为一个epoch。如果epoch设置太大,模型就会出现over fitting的现象。 |
6. 实现一个神经网络
手搓一个GPT太难了,先手搓一个神经网络吧。这次还是用mnist手写数字的数据集。
6.1 加载训练数据集
我们利用keras库加载mnist手写体数据。然后用pyplot把这些图片展示出来。
from keras.datasets import mnist
import matplotlib.pyplot as plt
(train_X, train_y), (test_X, test_y) = mnist.load_data()
sample_X = []
# 展示前9张图
for i in range(9):
plt.subplot(330 + 1 + i)
plt.imshow(train_X[i], cmap=plt.get_cmap('gray'))
plt.show()
# 原来的数据是二维的,我们把它展平成一维
for _train_x in train_X:
sample_X.append(_train_x.flatten())
6.2 设置神经网络模型
import torch.nn.functional as F
import torch.nn as nn
hidden1=100
hidden2=180
class NeuralNet(nn.Module):
def __init__(self):
super(NeuralNet, self).__init__()
# 神经网络的输入为784维,因为每张图片包含784个灰度值
self.fc1 = nn.Linear(784, hidden1)
self.fc2 = nn.Linear(hidden1, hidden2)
# 神经网络的输出维10维,因为对应0-9十个数字
self.fc3 = nn.Linear(hidden2, 10)
def forward(self, x):
y = torch.relu(self.fc1(x))
y = torch.relu(self.fc2(y))
y = nn.functional.softmax(self.fc3(y),dim=1)
return y
可以看到我们设置了一个包含2个隐藏层的神经网络,输入是784维图片灰度数据,输出是10维分别代表1-10十个数字。第一层和第二层隐藏层使用relu函数作为激活函数,输出层采用softmax将所有结果归一化。
6.3 开始训练模型
import torch.nn as nn
import torch.optim as optim
import torch
from tqdm import tqdm
# 创建一个输入张量
x = torch.tensor(sample_X, dtype=torch.float32)
# 创建标记的张量
y = torch.tensor(train_y, dtype=torch.int64)
# 损失值记录
losses=[]
# 将上边定义的模型实例化
model = NeuralNet()
# 使用Cross Entropy Loss作为损失函数
criterion = nn.CrossEntropyLoss()
# 随机梯度下降法(SGD)作为优化器
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 训练逻辑
for epoch in tqdm(range(150)):
# 旧梯度数据清零
optimizer.zero_grad()
# 前向传播过程
outputs = model(x)
# 计算损失并把损失值保存在数组里,方便后边查看训练情况
loss = criterion(outputs, y)
losses.append(loss.item())
# 后向传播过程,计算梯度
loss.backward()
# 更新模型参数
optimizer.step()
6.4 查看损失值
plot1=plt.plot(range(len(losses)),losses,'.',label='loss')
plt.xlabel('X axis')
plt.ylabel('Y axis')
plt.title('Loss')
plt.legend(loc=4)
plt.grid(True)
plt.show()
6.5 使用测试集计算准确率
total = len(test_X)
correct = 0
for i in range(total):
# 执行一次前向传播进行预测
predict = model(torch.tensor([test_X[i].flatten()],dtype=torch.float32))
# 提取概率最大的下标
predict_n = predict.argmax()
if predict_n == test_y[i]:
correct = correct+1
print(str(correct/total))
0.8982
测试结果是接近90%,还可以继续优化。