NLP入门之路(1):线性神经网络
从这一章开始,笔者将总结自己从一个人工智能、机器学习0基础的小白到学会NLP领域的前沿模型的过程,包括理论部分和代码相结合的方式。适合理论与代码都啥也不会的小白同学进来学习(笔者的学习书籍是李沐老师的动手学深度学习,但是笔者学完以后觉得对新手还是不太友好,因此对李沐老师的课程进行了二次提炼,希望对笔者初学时那样的新手都能学的明白)
(原课程笔者前前后后学了大概两三个月,感觉李沐老师说的很多东西并不适合新手小白,因此笔者自己重新整理了学习的过程,把看课程时踩过的坑都提了出来,因为手写打字实在是太麻烦,笔者自己录了相对应的配套视频,在视频里详细的对每一步的公式都进行了手动推导,并且解释了新手可能不明白的函数的意义以及为什么这么使用,为大家节省更多的时间,让知识不再抽象!!!)
(文章的开头有笔者自己录的讲解视频,大家可以配套查看,关于后面的代码部分,对于调用包中的函数不明白的同学可以gpt一下,用多了就懂了,强烈建议新手不要深究,知道怎么用的就可以了!!!)
这是李沐老师的动手学深度学习https://zh-v2.d2l.ai/
这是配套视频的链接https://www.bilibili.com/video/BV1dx4y1e7DQ/?vd_source=2db250394aa12deb4610d9a67bc4be5f
线性回归
简单的线性回归可以理解成我们初高中学习的一元一次函数或多元一次函数。
如:
y
=
k
x
+
b
y=kx+b
y=kx+b
这个上述的y就是我们机器学习要去拟合的最终结果,而x就是输入的值,b就是偏差。与初高中学习不同的是,在实际的拟合过程中,我们不仅仅只跟一个x有关,还可能与很多因素都有关系。例如我们的房价可能与面积大小,卧室数量等可以转化成数字量的因素都有关系,那么这个房价就是一个综合的结果。这个时候
y
=
k
1
x
1
+
k
2
x
2
+
⋯
+
k
n
x
n
y=k_{1}x_{1}+k_{2}x_{2} +\cdots+k_{n}x_{n}
y=k1x1+k2x2+⋯+knxn
因此为了很好的表示从
x
1
∼
x
n
x_{1} \sim x_{n}
x1∼xn,我们想到了使用大学数学中的矩阵去表示,因此,今后的学习中,我们都将使用矩阵
[
x
1
⋯
x
n
]
\begin{bmatrix} x_{1} \cdots x_{n}\end{bmatrix}
[x1⋯xn]来表示我们要输入的变量
X
X
X。而
[
k
1
⋯
k
n
]
\begin{bmatrix}k_{1} \cdots k_{n}\end{bmatrix}
[k1⋯kn]矩阵
K
K
K则被称为权重,这里我们使用
W
W
W来代替,而
b
\mathbf{b}
b仍然是偏差
b
i
a
s
bias
bias。因此我们新的线性回归函数则可以使用
y
=
W
T
⋅
X
+
b
y=W ^{T}\cdot X + b
y=WT⋅X+b来表示,在上述的等式中
W
T
W^{T}
WT是一个1行n列的行向量,而
X
X
X则是一个n行1列的列向量。因为最后的y仅仅只是一个数,所以需要注意输入和输出的格式。
OK,刚刚我们简要的解释了线性回归的概念,现在大家应该比较清楚了,理论上上述的n应该是一个无穷大的数,这样才能包含所有的可能性,那么在现实生活中,我们可以使用的数据往往是有限的,因此最后得出的y并不是真实值,只是我们的一个预测值。这里我们使用
y
^
\hat{y}
y^(带尖角的y,称为y_hat)来表示预测值。
损失函数
经过上述小节的介绍,我们应该知道了,所谓的线性回归任务,就是使用有限的x去预测真实的y,在机器学习的过程中,机器回去自动调整权重,使得当你输入一个x时,它处理出的输出y能最大可能的接近真实值。从上图可以看出,我们预测出的最终是一个函数,但是真实值往往都是散点,因此不可能所有的真实值都落在相对应的函数上,因此我们该如何衡量预测函数的好坏呢?换句话说,你的机器该按照什么标准自己去判断然后自动的修改呢。因此我们提出了一个叫损失函数的概念,我们定义 l o s s = 1 2 ( y ^ i − y i ) 2 loss = \frac{1}{2}(\hat{y}^{i}-y^{i})^2 loss=21(y^i−yi)2,现在我们解读一下这个函数,其实很简单,就是一个二次函数,代表所有预测值和真实值差值平方的和再除以二分之一,这里的二分之一我们放到后面再说,因为常数不影响。上述公式代表一个预测值和真实值之间的损失,那么对于n个数而言,将上述公式修改为 l o s s = 1 n ∑ i = 1 n 1 2 ( y ^ i − y i ) 2 loss=\frac{1}{n}\sum_{i=1}^n\frac{1}{2}(\hat{y}^{i}-y^{i})^2 loss=n1∑i=1n21(y^i−yi)2,即求出所有的损失并求出其平均值。现在我们更改一下 L ( w , b ) = l o s s L(w,b)=loss L(w,b)=loss,使用 L ( w , b ) L(w,b) L(w,b)来表示损失函数, L ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w i x i + b − y i ) 2 L(w,b)=\frac{1}{n}\sum_{i=1}^n\frac{1}{2}(w_{i}x_{i}+b-y^{i})^2 L(w,b)=n1∑i=1n21(wixi+b−yi)2其中 w , b w,b w,b分别表示参数,也就是我们需要求的值,以后我们都将这样表示。而我们的最终目的,就是求出一组参数 ( w ∗ , b ∗ ) (w^{*},b^{*}) (w∗,b∗),使得我们的损失在输入的所有数据上求出的损失值最小。即 w ∗ , b ∗ = arg min w , b L ( w , b ) w^*,b^*=\argmin\limits_{\displaystyle w,b}L(w,b) w∗,b∗=w,bargminL(w,b)
解析解
在上述小节中,我们提到了,我们的最终目标就是找到使得函数取得最小值的 w , b w,b w,b,那么不免想到使用求导,请注意,上述中的 w , x w,x w,x都是行向量,其中 x x x中的每一项代表一个因素,我们也称为维度, w w w中的每一项则代表对应因素的系数,我们也成为权重。(这里涉及到有关向量、矩阵的求导法则,大家感兴趣的可以自己搜一下,这里不做详细介绍,毕竟后面都交给计算机处理的)。因此我们对上述 L ( w , b ) L(w,b) L(w,b)函数进行求导,记住这里参数有两个,所以要分别求导,但是b是常数。再设置导数为0,最终 w ∗ = ( X T X ) − 1 X T y w^*=(X^TX)^{-1}X^Ty w∗=(XTX)−1XTy,我们将上述解称为解析解,像线性回归这样的简单问题存在解析解,但并不是所有的问题都存在解析解。 解析解可以进行很好的数学分析,但解析解对问题的限制很严格,导致它无法广泛应用在深度学习里。
随机梯度下降
上述的
L
(
w
,
b
)
L(w,b)
L(w,b)其实在后面的过程中称之为模型,只不过这个模型比较简单,对于未来的复杂模型,我们可能大概率是得不到解析解的,这个时候,有一个行之有效的方法,称之为随机梯度下降,这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。在每次迭代中,我们首先随机抽样一个小批量
B
\Beta
B,它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数
η
\eta
η,并从当前参数的值中减掉。用数学公式表示就是(其中
∂
\partial
∂代表偏导数)
(
w
,
b
)
←
(
w
,
b
)
−
η
∣
B
∣
∑
i
=
1
∣
B
∣
∂
(
w
,
b
)
l
o
s
s
(w,b)\leftarrow(w,b)-\frac{\eta}{\lvert \Beta\rvert}\sum_{i=1}^{\lvert \Beta\rvert}\partial_{(w,b)}loss
(w,b)←(w,b)−∣B∣ηi=1∑∣B∣∂(w,b)loss
在接下来的过程中,我们将使用该方法来进行参数的优化,还有很多其他的优化方法,我们放到后面说,感兴趣的同学也可以自己搜一下,不影响下面的学习过程。
从零实现线性回归
接下来,笔者将从代码的角度,一步一步带大家实现线性回归。(本章我们使用的工具是pytorch,大家可以自行安装一下,笔者后期将出一个从Anaconda到pytorch以及配置到pycharm中的详细教程)。
首先我们要生成自己的一个小数据集,那么我们假设我们的维度有2个吧,那么相对应的权重也就2个。那首先我们确定一下假设我们的真实数据是由权重
w
=
[
2
,
−
3.4
]
、
b
=
4.2
w=[2,-3.4]、b=4.2
w=[2,−3.4]、b=4.2生成,但是为了更好的进行实验,我们为我们的数据生成相对应的噪声
ϵ
\epsilon
ϵ,因此最后我们的函数应该如下
y
=
X
w
+
b
+
ϵ
y=Xw+b+\epsilon
y=Xw+b+ϵ在这里我们认为标准假设成立,即
ϵ
\epsilon
ϵ服从均值为0的正态分布。 为了简化问题,我们将标准差设为0.01。 下面的代码生成合成数据集。
def synthetic_data(w, b, num_examples):
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
好,那么上述的features以及labels就代表我们的真实数据x以及y。
在机器学习的过程中,模型要做的一件很重要的事情就遍历数据集,为了代码的简洁性,我们首先要做一个遍历数据集的函数date_iter,该函数的接受参数为批量大小(batch_size),features,labels,它会返回批量大小的特征(features)和标签(labels)
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
做完这一切后,我们先初始化一下我们要求的模型参数
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
因为后面要计算梯度,也就是求导数,因此这里requires_grad=True。
接下来我们按顺序定义我们需要的函数
首先是我们最初用公式表示的线性回归模型
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
接下来是损失函数
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
#返回的是一个矩阵形式,长度和y_hat相同
之后是我们的优化函数,优化模型参数用的(其中lr便是上述公式中的 η \eta η)
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
"""这里必须param.grad.zero_(),如果不清零,则pytorch的梯度会叠加,
清零后计算的就是每一次的梯度"""
有了上述函数之后,我们需要进行的便是训练,在训练前,我们需要设置一些超参数,这些参数是不用训练出来人为设置的,但是超参数的设置也会影响结果的好坏,因此也是需要我们不停的进行设置和实验才能得到最佳效果,这里就不做详细实验。该实验中,我们的超参数是训练的轮数num_epochs以及学习率lr
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损失
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward()
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
线性回归简洁实现
上述实现确实是很麻烦的一件事,我们需要自己手动实现一个一个函数,这就是为什么我们需要pytorch,因为他为我们封装了深度学习中很多重要且常用的函数,我们可以直接调用,只需要记住输入和输出的形状就行了。
首先是生成数据集
import numpy as np
import torch
from torch.utils import data
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
下面我们将实现读取数据集
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
定义我们的模型
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
#nn.Linear是将输入为(n,2)形状的张量变为(n,1),内部实现了矩阵乘法,其中的w就是我们的参数
#nn.Sequential可以认为是一个列表,依次放入这些层
模型都定义好了,那么参数肯定是必不可少的,但是使用了nn这个包以后,参数都封装在里面,而不是像自己写那样暴露在外面
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
再后面就是损失函数
loss = nn.MSELoss()
之后便是我们的优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
最后有了上述函数后便是进行我们的训练过程
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
以上就是本章的全部内容,第一次做不太熟练,有疑惑的地方在评论区讨论,欢迎指正!!!