NLP-深度学习(一)

NLP | 深度学习(一)

一. 深度学习简介

1. 神经元

在神经网络中,单个神经元,其实就是对上一层输入过来的K个值先进行线性运算,后使用激活函数进行非线性性运算的过程。图示化如下:

image-20201124212319523

2. 全连接神经网络:

此处,神经网路由一层一层的神经元彼此相连组成。全连接:神经网络中上一层的每个神经元和下一层的每个神经元之间都有数据交互。一个简单的示例如下:

image-20201124212953021

一个简单的两层全连接神经网络的计算过程如下图所示:

image-20201124213054768

此处,输入层向量为(1,-1),在经过参数权值\(w_1\):(1,-2)和(-1,1)【见边上的数字】分别抵达了第一层隐藏层的两个神经元,得到结果为(3,-2)【加权和】。然后加上各自神经元对应的偏置权重\(b_1\):(1,0),得到了(4,-2)。以上是线性运算的步骤。

之后经过了一个激活函数\(\sigma(x) = \frac{1}{1+e^{-x} }\),得到了该层隐藏层的最终输出值(0.98,0.12),这个继续传递到下一层进行类似的运算。直到抵达输出层,输出网络的最终结果:(0.62,0.83)。

4. 为什么要多层:

深度学习使用的就是这样的一个多层神经网络,他需要根据训练数据,寻找一组合适的参数权重w和偏置权重b。

然而我们从这个网络的计算方式中可以看出,他其实就是一个函数:\(F = \sigma(W_3*\sigma(W_2*\sigma(W_1*x)))\)。而在早期就已经有科学家们提出:任何一个从$R^N \(映射到\) R^M$的连续函数都可以用仅含有一个隐藏层的神经网络得到。那么为什么要使用多层呢?

想要使用仅含有一层的神经网络\(N_1\)来实现多层的神经网络\(N_x\)的功能,就需要\(N_1\)的隐藏层具有更多甚至多得多的神经元,这会导致网络模型显得十分臃肿;另外一个使用多层神经网络的作用就是:有时我们需要将一个项目差分成几个子任务模块来实现。为了让大家能够直观的理解,给出一个并不准确例子:拿手人脸识别举例,不同层关注的区域可能不同,可能前几层关注的是面部轮廓,在后几层关注的是肤色信息,之后几层有关注了更细致的眼耳鼻口舌等特征;还有我觉得的最有说服力的一点:深层神经网络的效果更加优异。

5. 神经网络的三个阶段:

可以说神经网络的工作方式就是基于以下三个阶段完成:定义函数集合→根据任务定义每个函数的损失函数→寻找最优函数。

[1]. 上面叙述的内容其实旨在表明一件事:神经网络本质就是一个函数,只是我们不知道适合我们当前任务的网络参数罢了。当我们定义了输入层的维度 i 和输出层的维度 o,就完成了函数集合定义的过程:\(F = \{ f | f: R^i \rightarrow R^o\}\)

[2]. 损失函数:根据任务的不同,我们的得到的损失也是各有差异。但大体的思路还是基本一样:根据网络得到的输出值和数据集的真实标签,使用均方差或是交叉熵等数学公式,定义损失函数,计算每个函数(网络)的损失值。

[3]. 寻找最优函数:在这一部分的工具我们常称之为优化器,一个简单的方法就是梯度下降法。梯度的几何意义是:梯度的方向指向了:函数增长的最快的那个方向。而我们的损失函数是越小越好,所以,我们就需要用当前的参数值减去一个加权(\(\alpha\))的梯度。使得,网络参数朝向使得网络损失下降的方向改变。

加权\(\alpha\)\(\alpha\)又被称作是学习率。如果我们用grad表示梯度,那么我们参数W更新至:\(W_{t+1} = W_t - \alpha*grad(W_t)\)。学习率的作用是:设置参数更新的步长,防止防止参数更新的值较大,导致离极小值点越来越远;或是参数每次更新的值太小,训练过程缓慢。一个直观的理解就是:如下图所示,对于一个一元二次函数,若当前参数位于点(1),若更新步长较大到了点(3),就会每次更新的离极小值点越来越远(距极小值点越远,导数越大);若是每次步长太小,又会导致每次更新的太慢(2)。梯度值我们不可控,所以就是用设置学习率来进行控制每次更新的步长。

image-20201125140943446

二. 神经网络:手写数字识别

1. 搭建流程

对于使用神经网络完成一个任务来说,通常可以按照如下几个步骤进行:

数据加载 \(\rightarrow\) 网络结构设置 \(\rightarrow\) 训练 \(\rightarrow\) 验证 \(\rightarrow\) 预测

下面将以手写数字识别任务为例,对每一部分进行讲解。

2. 数据加载

1
2
3
4
epochs = 1
batch_size = 64
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
learning_rate = 0.001

epochs:训练轮数

batch_size:一次训练中为网络送入的数据条数

device:判断是否有空闲的GPU,没有就用cpu

learning_rate:学习率(\(\alpha\))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# 加载或下载后加载训练集
train_dataset = datasets.MNIST(root='./data/',train=True,
transform=transforms.ToTensor(),download=True)
# 加载或下载后加载测试集
test_dataset = datasets.MNIST(root='./data/', train=False,
transform=transforms.ToTensor(),download=True)

#建立一个数据迭代器
# 装载训练集
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
# 装载测试集
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=True)

参数介绍:

  • datasets.MNIST: 加载或下载后加载数据集

    root: 数据集在下载之后的存放路径;

    transform:导入数据集时需要数据格式,此处定义为Tensor;

    train: True 表示训练集部分,False 表示测试集部分;

    download:True 表示若存放路径中不存在mnist数据集,则下载到对应路径中并加载数据。

  • DataLoader:数据迭代器,每次访问会从dataset参数指定的数据集中返回一个batch_size大小的数据

    shuffle:是否打乱数据集(True / False)

3. 网络搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Model(nn.Module):
def __init__(self, categories_num = 10):
super(Model, self).__init__()
self.categories_num = categories_num
self.fc1 = torch.nn.Linear(28*28, out_features =100)
self.fc2 = torch.nn.Linear(100, 100)
self.out = torch.nn.Linear(100, self.categories_num)

def forward(self, x):
x = self.fc1(x)
x = torch.relu(x)
x = self.fc2(x)
x = torch.relu(x)
x = self.out(x)
x = torch.softmax(x, dim=1)
return x

这一部分,将搭建好的网络封装到一个类中。网络一共三层全连接层:第一层fc1输入维度为图片分辨率:28*28,输出维度自定义:100;其后隐藏层fc2的输入为fc1输出,故输入维度:100,输出维度自定义:100;输出层out的输入为fc2的输出,故维度:100,输出应该为类别(0~9)数:10。

在前向传播中,前两层使用relu激活函数,最后一层使用softmax激活函数。由于此处x是二维矩阵[batch_size, 10],进行softmax时要指定是在矩阵第几维度上进行。dim = 1指定:按行计算。

4. 训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def train(model, optimizer, criterion):
for epoch in range(epochs):
sum_loss = 0.0
for i, data in enumerate(train_loader):
inputs, labels = data
inputs, labels = inputs.reshape(inputs.size()[0],-1).cuda(), labels.cuda()
optimizer.zero_grad() # 将梯度归零
outputs = model.forward(inputs) # 将数据传入网络进行前向运算
loss = criterion(outputs, labels) # 得到损失函数
loss.backward() # 反向传播
optimizer.step() # 通过梯度做一步参数更新
sum_loss += loss.item()
if (i+1) % 100 == 0:
print('[%d,%d] loss:%.03f' %(epoch + 1, i + 1, sum_loss / 100))
sum_loss = 0.0

一次epoch指的是:使用训练集中所有数据为网络参数训练一次。在该过程,我们遍历数据迭代器train_loader,每次获得一个batch_size大小的数据和对应标签。但如果数据集数据条数不是batch_size的整数倍,该轮epoch最后一次训练返回的就是剩余所有数据。我们在进行输入数据格式转换时,inputs.size()[0]获取了数据条数,使用了reshape方法指定转换后的二维矩阵第一维度是inputs.size()[0],第二维度的-1表示:reshape函数其他参数计算出该维度值。

optimizer.zero_grad()将当前已累计的梯度值清零。torch在获得了数据的真实标签和网络的预测标签后,使用指定的损失函数计算损失loss,并使用loss.backward()计算每一层神经元的梯度并存放到动态图中。此时网络参数尚未更新。当使用optimizer.step()方法后,会根据当前已累计的梯度值与学习率更新网络参数。

最后我们每100词迭代,就输出一次平均损失。

5. 验证

训练过程可能会出现过拟合现象,所以我们需要使用验证集验证模型好坏。选出训练过程中性能最好的模型用于实际的预测任务。此处我们直接使用mnist的测试集进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def valid(model):
model.eval() # 将模型变换为测试模式
correct = 0
total = 0
for data_test in test_loader:
images, labels = data_test
images, labels = images.reshape(images.size()[0], -1).cuda(), labels.cuda()
output = model.forward(images)
_, predicted = torch.max(output, 1)
total += labels.size(0)
correct += np.sum((torch.Tensor.cpu(predicted).numpy() == torch.Tensor.cpu(labels).numpy()))
print("Test acc: ",correct / len(test_dataset))

if __name__ == '__main__':
model = Model().to(device)
# 交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 优化器Aadam
optimizer = optim.Adam(
model.parameters(),
lr=learning_rate,
)
train(model, optimizer, criterion)
valid(model)

6. 测试

根据验证阶段得到的最好的模型,我们可以对一个新的手写数字图片进行识别,具体操作和valid中的类似。