NLP中的神经网络基础

目录

一:多层感知器模型

1:感知器

2:线性回归

3:Logical回归

4:Softmax回归

5:多层感知器(MLP)

6:模型实现

(1):神经网络层和激活函数

(2):自定义神经网络模型

二:卷积神经网络(CNN)

1:模型结构

2:模型实现

三:循环神经网络

1:RNN模型结构

2:长短记忆网络(LSTM)

3:模型实现

4:基于循环神经网络的序列到序列模型

四:注意力模型

1:注意力机制

2:自注意力模型

3:Transformer

(1):融入位置信息

(2):输入向量角色信息

(3):多层注意力

(4):自注意力计算结果互斥

4:Transformer模型实现

五:神经网络模型的训练

1:损失函数

2:梯度下降

3:梯度下降算法代码实现



一:多层感知器模型

1:感知器

解释一下,为什么写成 wx+b>0 ,其实原本是 wx > t ,t就是阈值,超过这个阈值fx就为1,现在把t放在左边。

在感知器里面涉及到两个问题:

        第一个,特征提取:就是如何将问题的原始输入转换成输入向量x(其实就是数值如何表示文本)

        第二个,参数学习(参数优化或者模型训练):就是如何设置合理的w权重和b偏差项

感知器主要处理文本分类问题,比如识别一个句子的褒贬性。

2:线性回归

感知器输出的结果是离散的,除了感知器这类分类模型还有一类是回归模型(Regression),他的输出是连续的实数值。线性回归是最简单的回归模型。y = wx + b

3:Logical回归

线性回归的输出值大小是任意的,激活函数就是为了将其限制在一定的范围内

Logical函数形式为:

        y = L/ 1 + e^{-k(Z-Z_{o})}  其中,k控制了函数的陡峭程度。

如果 z = w_{1}x_{1} + w_{2}x_{2} + ...,此时的模型叫做Logical回归模型。虽然叫做回归模型但是常用作分类问题

原因:当L=1、k=1,z0 = 0 ,此时函数形式就是sigmoid函数

它的值域恰好在0-1,所以经过sigmoid函数归一化以后就可以认为是输入属于某一类别的概率值,除了可以输出概率值还有一个优点就是它的导数比较容易求得,有利于使用基于梯度的参数优化算法。sigmoid函数图像如下图:

4:Softmax回归

如果不只有2个类别,处理多元分类任务可以用Softmax回归操作。

Softmax回归就是对第i个类别使用线性回归打一个分数z_{i} = w_{i1}x_{1} + w_{i2}x_{2} + ... + w_{in}x_{n} + b_{i}。其中,w_{ij}表示第i个类别对应的第i个输入的权重。然后再对多个分数使用指数函数进行归一化计算,并获得一个输入属于某个类别的概率。

5:多层感知器(MLP)

以上介绍的都是线性模型,真实情况很多时候无法通过一条直线、平面、或者超平面来分割不同的类别,例如:异或问题(相同为0,不同为1)

多层感知器就是堆叠多层线性分类器,并在中间(隐含层)增加非线性激活函数

常见激活函数可以参考:常见激活函数(Sigmoid、Tanh、Relu、Leaky Relu、Softmax)_sigmoid激活函数-优快云博客

ReLU:

6:模型实现

(1):神经网络层和激活函数

线性层:输入为(batch,inputdim)输出为(batch,outputdim)

# 线性层
import torch
from torch import nn
linear = nn.Linear(32, 2)   # 第一个参数就是输入,第二个是输出
inputs = torch.rand(3, 32)  # 创建一个输入为3, 23 维度的随机张量  可以理解为3是batch
outputs = linear(inputs)
print("Linear:")
print(outputs)

batch就是一个批次,即为一次处理的张量数量。

这里grad_fn是张量(tensor)的一个属性,它记录了创建该张量的函数操作。

激活函数在torch.nn.functional中
 

# test_1
import torch
from torch import nn
linear = nn.Linear(32, 2)   # 第一个参数就是输入,第二个是输出
inputs = torch.rand(3, 32)  # 创建一个输入为3, 23 维度的随机张量  可以将3理解为batch 就是批次大小
outputs = linear(inputs)
print("Linear:")
print(outputs)
print("*****"*20)


from torch.nn import functional as F
activation_sigmoid = F.sigmoid(outputs)
print("activation_sigmoid:")
print(activation_sigmoid)
print("*****"*20)

activation_softmax = F.softmax(outputs, dim=1)  # 沿着第2维度进行Softmax计算,即对每批次中的各样例进行Softmax运算
print("activation_softmax:")
print(activation_softmax)
print("*****"*20)

activation_relu = F.relu(outputs)
print("activation_relu:")
print(activation_relu)
print("*****"*20)

activation_tanh = F.tanh(outputs)
print("activation_tanh:")
print(activation_tanh)
print("*****"*20)

(2):自定义神经网络模型

# test_2
import torch
from torch import nn
from torch.nn import functional as F

class MLP(nn.Module):
    # 多层感知器的构建
    def __init__(self, input_dim, hidden_dim, num_class):
        super(MLP, self).__init__()
        # 线性变换:输入层-->隐含层
        self.linear1 = nn.Linear(input_dim, hidden_dim)

        # ReLU
        self.activate = F.relu

        # 线性变换:隐藏层-->输出层
        self.linear2 = nn.Linear(hidden_dim, num_class)

    def forward(self, inputs):
        # 输入->隐含层1输出->激活函数输出->隐含层2输出->Softmax输出
        hidden = self.linear1(inputs)  # 隐含层1输出
        activation = self.activate(hidden)  # 激活函数输出
        outputs = self.linear2(activation)  # 隐含层2输出
        probs = F.softmax(outputs, dim=1)  # Softmax输出 获得每个输入属于某一类别的概率
        return probs


mlp = MLP(input_dim=4, hidden_dim=5, num_class=2)
inputs = torch.rand(3, 4)  # 3个输入  每个输出维度为4
probs = mlp(inputs)
print(probs)

二:卷积神经网络(CNN)

1:模型结构

全连接层:又叫做稠密层,在多层感知器中每层输入的各个元素都需要乘以一个独立的参数的那一层叫做全连接层。 缺点:难以捕捉局部信息。

卷积操作:就是依次扫描输入的每个区域。每个小的、用于提取局部特征的稠密层被叫做卷积核或者滤波器。假设卷积核的大小为N,单词长度为L,那么卷积核的输出长度为L-N+1

卷积操作输出的结果再进行进一步聚合,这一过程就是池化。池化包括最大池化、平均池化、加和池化等。池化的优点:解决样本输入大小不一致的问题;可以保证最终输出相同个数的特征。

卷积核的构造方式大致有两种:a:使用不同组的参数,且不同的初始化参数获得不同的卷积核。b:提取不同尺度的局部特征(例如:提取不同大小N-gram)

卷积操作以后再经过一个全连接的分类层就可以做出最终的决策。将多个卷积层池化层叠堆叠起来形成更深层次的网络就叫做卷积神经网络(CNN)

输入+卷积+池化+全连接

前馈神经网络:信息从输入层经过隐藏层再到输出层,按照一个方向流动,就叫做前馈神经网络

2:模型实现

输入数据形状为 (batch,in_channels,seq_len)输出数据形状为(batch,out_channels,seq_len)

# test_3
import torch
from torch.nn import Conv1d

# 卷积操作调用
conv1 = Conv1d(5, 2, 4)  # 输入通道大小为5:输入词向量维度为5 输出通道大小为2:输出通道个数为2 卷积核宽度为4
conv2 = Conv1d(5, 2, 3)

inputs = torch.rand(2, 5, 6)  # 输入批次为2 每个序列长度为6 每个输入的维度是5

outputs1 = conv1(inputs)
outputs2 = conv2(inputs)

print("outputs1:")
print(outputs1)
print(type(outputs1))  # 2 2 3 输出2批次 每个序列长度为3(6-4+1) 大小为2
print(outputs1.shape)
print("*****"*20)
print("outputs2:")
print(outputs2)
print(outputs2.shape)  # 2 2 4 输出2批次 每个序列长度为4(6-3+1) 大小为2
print("*****"*20)


# 池化操作调用
from torch.nn import MaxPool1d

pool1 = MaxPool1d(3)  # 池化层核大小为3 即为卷积层的输出序列长度
pool2 =  MaxPool1d(4)

outputs_pool1 = pool1(outputs1)
outputs_pool2 = pool2(outputs2)

print("outputs_pool1:")
print(outputs_pool1)
print(type(outputs_pool1))
print(outputs_pool1.shape)  # 2 2 1(3-3+1)
print("*****"*20)
print("outputs_pool2:")
print(outputs_pool2)
print(outputs_pool2.shape)  # 2 2 1(4-4+1)
print("*****"*20)


# 将两个池化后的张量合在一起
outputs_pool_squeeze1 = outputs_pool1.squeeze(dim=2)  # 去掉最后一个维度  2 2
outputs_pool_squeeze2 = outputs_pool2.squeeze(dim=2)  # 2 2
outputs_pool = torch.cat([outputs_pool_squeeze1, outputs_pool_squeeze2], dim=1)  # 变为 2 4

# 最后全连接层
from torch.nn import Linear
linear = Linear(4, 2)
outputs_linear = linear(outputs_pool)
print("outputs_linear:")
print(outputs_linear)
print(outputs_linear.shape)  # 2 2
print("*****"*20)

注意维度变化:其中输入输出的时候 最后两个维度需要用矩阵的想法去思考


三:循环神经网络

多层感知器和卷积神经网络信息都是按照一个方向流动,因此都属于前馈神经网络,下面介绍信息循环流动的网络。

1:RNN模型结构

循环神经网络,将其展开后就相当于堆叠多个共享隐藏层参数的前馈神经网络。隐藏层更新的方式可以用以下的公式进行表达:

h_{t} = tanh(W^{xh}x_{t} + b^{xh} + W^{hh}h_{t-1} + b^{hh})

y = Softmax(W^{hy}h_{n} + b^{hy})

其中, tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}}

每个时刻的隐藏层 h(t) 承载了 1~t 时刻的全部信息,因此循环神经网络中的隐藏层也被称为记忆单元

2:长短记忆网络(LSTM)

长短记忆网络将隐藏层的更新方式改为

u_{t} = tanh(W^{xh}x_{t} + b^{xh} + W^{hh}h_{t-1} + b^{hh})

h_{t} = h_{t-1} + u_{t}

但是,可能要考虑两个状态的比率,所以需要添加一个 ft 系数来控制加权(遗忘门

就可以将公式改为

f_{t} = Sigmoid(W^{f,xh}x_{t} + b^{f,xh} + W^{f,hh}h_{t-1} + b^{f,hh})

h_{t} = f_{t} * h_{t-1} + (1 - f_{t})u_{t}

但是,如果两种状态需要独立的话,就需要使用独立的系数来控制。这里的it系数就叫做输入门i_{t} = Sigmoid(W^{i,xh}x_{t} + b^{i,xh} + W^{i,hh}h_{t-1} + b^{i,hh})

h_{t} = f_{t} * h_{t-1} + i_{t}u_{t}

同样,还可以增加输出门控制机制

i_{t} = Sigmoid(W^{i,xh}x_{t} + b^{i,xh} + W^{i,hh}h_{t-1} + b^{i,hh})

c_{t} = f_{t} * c_{t-1} + i_{t}u_{t}

h_{t} = i_{t} * tanh(c_{t})

这里的ct被叫做记忆细胞。

3:模型实现

维度变化需要注意:

这里的后两个维度和前面卷积神经网络理解不一样,理解为每个序列长度每时刻输入的大小

CNN实现:

# test_4
from torch.nn import RNN
import torch

rnn = RNN(input_size=4, hidden_size=5, batch_first=True)  # 每个时刻输出大小为 4 隐藏层大小为 5 batch_first代表第一个是否为batch
inputs = torch.rand(2, 3, 4)  # 数据批次大小为2 即为每次输入2个序列 每个序列的长度是3 每个时刻输入的大小为4
outputs, hn = rnn(inputs)  # 输出有两个 一个是输出序列的隐含层outputs   还有一个是最后一时刻的隐含层hn

print("outputs:")
print(outputs)
print(type(outputs))  # 2 3 5  2个批次 每个序列长度为3 隐含层大小为5
print(outputs.shape)
print("*****"*20)

print("hn:")
print(hn)
print(type(hn))  # 1 2 5  1无含义 批次大小为2 隐含层大小为5
print(hn.shape)
print("*****"*20)

初始化RNN的时候还可以通过设置其他参数修改网络结构,例如:bidirectional = True就是双向RNN,num_layers可以设置循环神经网络的堆叠层数。

LSTM实现:

# test_4
from torch.nn import LSTM
import torch

lstm = LSTM(input_size=4, hidden_size=5, batch_first=True)
inputs = torch.rand(2, 3, 4)
outputs, (hn, cn) = lstm(inputs)

print("outputs:")
print(outputs)
print(type(outputs))  # 2 3 5
print(outputs.shape)
print("*****"*20)

print("hn:")
print(hn)
print(type(hn))  # 1 2 5
print(hn.shape)
print("*****"*20)

print("cn:")
print(cn)
print(type(cn))  # 1 2 5
print(cn.shape)
print("*****"*20)

LSTM多了一个记忆细胞 cn

4:基于循环神经网络的序列到序列模型

循环神经网络除了可以处理分类问题和序列标注问题,还能处理序列到序列的理解和生成,相应的模型叫做编码器-译码器模型

首先编码器使 用循环神经网络进行对源语言的编码,然后在以最后一个单词对应的隐含层作为初始,再调用解码器(另一个循环神经网络)逐词生成目标语言的句子。

基于循环神经网络的序列到序列模型有一个基本的假设,就是原始序列的最后一个隐含状态(一个向量)包含了该序列的全部信息。显然是不合理的,注意力模型就是解决这个问题的。

四:注意力模型

1:注意力机制

注意力权重计算公式:

\alpha_{s}^{'}= attn(h_{s}, h_{t-1})

\alpha_{s} = Softmax(\alpha^{'})_{s}
hs代表源序列中s时刻的状态,ht-1表示目标序列前一个时刻状态,attn是注意力计算公式,\alpha^{'} = [\alpha_{1}^{'}, \alpha_{2}^{'}, ... , \alpha_{L}^{'}],其中L为源序列长度,最后对整个源时刻序列每个时刻的注意力分数使用Softmax函数进行归一化获得最终权重\alpha_{s}

2:自注意力模型

当要表示序列某一时刻的状态时,可以通过该状态与其他时刻状态之间的相关性(注意力)来计算,这就叫做自注意力机制。

假设有输入为n个向量组成的序列xi,输出为每个向量对应的新的向量表示yi,其中所有向量的大小都为d,那么计算yi的公式如下:

y_{i} = \sum_{j=1}^{n}\alpha _{ij}x_{i},其中\alpha _{ij}是xi和xj之间的注意力计算值,直观含义就是xi和xj如果越相关,则他们计算的注意力值就越大。通过自注意力机制可以计算两个距离较远的时刻之间的关系,但是在循环神经网络中不可以。

3:Transformer

自注意力模型问题:

a:计算自注意力时,没有考虑输入的位置信息,没办法对序列进行建模。

b:输入向量xi承担多个角色,计算注意力权重时的两个向量以及被加权的向量,导致不易被学习。

c:只考虑了两个输入序列单元之间的关系,无法建模多个输入序列单元之间更复杂的关系。

d:自注意力计算结果互斥,无法同时关注多个输入。

(1):融入位置信息

原始的自注意力模型没有考虑输入向量的位置信息,导致其与词袋模型类似,两个句子只要包含相同的词语,向量表示就会相同。为了解决可以使用位置嵌入(Position Enbeddings)位置编码(Position Encodings)

位置嵌入:为序列中每个绝对位置赋予一个连续、低维、稠密的向量表示。

位置编码:使用函数将一个整数映射在一个d维的向量上。

位置向量和位置编码都是获得一个位置对应的向量后,再与该位置对应的词向量进行相加,即可表示该位置的输入向量。

(2):输入向量角色信息

原始的自注意力模型在计算注意力值的时候直接使用两个向量,然后使用得到的注意力对同一个输入向量加权,这样导致一个输入向量既是Query、Key又是Value。更好的做法应该是使用不同的参数矩阵对原始的输入向量做线性变化,应该让不同的参数矩阵w^{q}/w^{k}/w^{v}将输入x_{i}映射为新的向量,然后再计算。

(3):多层注意力

原始的自注意力模型仅考虑了序列中任意两个输入序列之间的关系,如果需要考虑多个,直接建模高阶关系会导致模型复杂度过高。我们可以通过堆叠多层自注意力模型实现,但是直接直接堆叠模型最终还是线性的,为了增强模型的表示能力,往往会在每层自注意力计算之后,增加一个非线性的多层感知器(MLP)模型。为了使模型学习更容易可以使用归一化层(Layer Normalization)、残差连接(Residueal Connections),这样就构成了Transformer块。

(4):自注意力计算结果互斥

在自注意力计算时,最后的结果会进行归一化,导致即使一个输入和多个其他输入相关,也无法同时为这些输入赋予较大的注意力值,即注意力结果之间是互斥的,无法同时关注多个输入。那么我们只需要设置多组映射矩阵然后将产生的多个输出拼接,再接一个线性映射,映射回d维度向量,该模型就叫做多头自注意力模型。可以理解为将不同的注意力头理解为抽取不同类型的特征

4:Transformer模型实现

# test_5
import torch
encoder_layer = torch.nn.TransformerEncoderLayer(d_model=4, nhead=2)  # 创建一个Transformer模块,每个输入向量、输出向量的维度是4,头数是2
scr = torch.rand(2, 3, 4)  # 序列长度  批次大小  输入向量的维度
out = encoder_layer(scr)

print("out:")
print(out)
print(type(out))  # 2 3 4
print(out.shape)
print("*****"*20)


# 将多个Transformer块堆叠起来,构成一个完整的nn.TransformerEncoder
transformer_encoder = torch.nn.TransformerEncoder(encoder_layer, num_layers=6)
out = transformer_encoder(scr)

print("out:")
print(out)
print(type(out))  # 2 3 4
print(out.shape)
print("*****"*20)

# 解码模块
memory = transformer_encoder(scr)
decoder_layer = torch.nn.TransformerDecoderLayer(d_model=4, nhead=2)
transformer_decoder = torch.nn.TransformerDecoder(decoder_layer, num_layers=6)
out_part = torch.rand(2, 3, 4)
out = transformer_decoder(out_part, memory)

print("out:")
print(out)
print(type(out))  # 2 3 4
print(out.shape)
print("*****"*20)

五:神经网络模型的训练

神经网络模型的训练其实就是寻找一组优化参数的过程

1:损失函数

损失函数就是用于衡量在训练数据集模型的输出真实输出之间的差异。

如果损失函数的值过于小,模型就会和训练数据过拟合(Overfit),可以通过正则化(Regularization)、丢弃正则化(Dropout)、早停法(Early Stoping)避免。

常见的两种损失函数:均方差(Mean Squared Error, MSE)损失和交叉损熵(Cross-Entropy,PE)损失。

均方误差损失:指的是每个样本的平均平方损失

MSE = \frac{1}{m}\sum_{i=1}^{m}(y\hat{}^{(i)} -y^{(i)} )^2

m为样本数量,y(i)为第i个样本的真实输出结果,带有帽标志的y(i)表示第i个样本的模型预测结果。

以上形式的均方误差使用于回归问题,即一个样本有一个连续输出值作为标准答案。

用均方误差损失处理分类问题,假设处理的是c分类的问题,可以定义为:

MSE = \frac{1}{m}\sum_{i=1}^{m}\sum_{j=1}^{c}(y_{j}\hat{}^{(i)} -y_{j}^{(i)} )^2

y_{j}^{(i)}代表第i个样本的第j类上的真实输出结果,只有正确的类别输出才是1,其他类别都是0。y_{j}\hat{}^{(i)}表示模型对第i个样本的第j个类上的预测结果,如果用Softmax函数对结果进行归一化,则表示对该类别预测的概率。

交叉熵损失

CE = -\frac{1}{m}\sum_{i=1}^{m}\sum_{j=1}^{c}y_{j}^{(i)}logy_{j}\hat{}^{(i)}

y_{j}^{(i)}代表第i个样本的第j类上的真实输出结果,只有正确的类别输出才是1,其他类别都是0。y_{j}\hat{}^{(i)}表示模型对第i个样本的第j个类上的预测结果。那么交叉熵损失只取决于模型对正确类别预测概率的对数。更本质上来说函数右侧是对多类输出结果的分布(伯努利分布)求极大似然中的对数似然函数。再加上交叉熵损失只取决于正确类别的预测结果,所以还可以简化为:

CE = -\frac{1}{m}\sum_{i=1}^{m}logy_{t}\hat{}^{(i)}

y_{t}\hat{}^{(i)}代表模型对第i个样本在正确类别t上的预测概率。所以交叉熵损失函数又叫负对数似然损失(NLL)。

2:梯度下降

梯度:就是以向量的形式写出的对多元函数各个参数求得的偏导数。梯度的物理意义是函数值增加最快的方向。

梯度下降算法:

input:学习率\alpha;含有m个样本的训练数据

output:优化参数\theta

流程:

设置损失函数L(f(x;\theta ),y)

初始化参数\theta

while 未达到终止条件do

        计算梯度g = \frac{1}{m}\bigtriangledown _{\theta }\sum_{i}^{m}L(f(x^{(i)};\theta ),y^{(i)})

        \theta = \theta - \alpha g

end

循环终止的条件可以是:给定的循环次数、算法两次循环之间梯度变化小于一定阈值、在开发集上算法的准确率不再上升等

当训练规模较大时,遍历全部的训练数据计算梯度算法时间会特别久。可以采用随机采样一定规模的训练数据来估计梯度,此时被称作小批次梯度下降。当小批次的数目被设置为b=1时,被称作随机梯度下降(SGD)

3:梯度下降算法代码实现

import torch
from torch import nn,optim
from torch.nn import functional as F

class MLP(nn.Module):
    # 构建多层感知器
    def __init__(self, input_dim, hidden_dim, num_class):
        super(MLP, self).__init__()
        self.linear_1 = nn.Linear(input_dim, hidden_dim)
        self.activate = F.relu
        self.linear_2 = nn.Linear(hidden_dim, num_class)

    def forward(self, inputs):
        hidden = self.linear_1(inputs)
        actovation = self.activate(hidden)
        outputs = self.linear_2(actovation)
        log_probs = F.log_softmax(outputs, dim=1)  # 获得输入属于某一类别的概率(Softmax),然后再取对数(log),取对数是为了避免计算Softmax产生的数据溢出问题。
        return log_probs

# 异或问题的4个输入
x_train = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
y_train = torch.tensor([0, 1, 1, 0])

model = MLP(input_dim=2, hidden_dim=5, num_class=2)
criterion = nn.NLLLoss()  # 当log_softmax输出时,需要调用负数对数似然损失NLL
optimizer = optim.SGD(model.parameters(), lr=0.05)  # 使用梯度下降参数优化方法,学习率参数设置为0.05


for epoch in range(500):
    y_pred = model(x_train)  # 计算预测值
    loss = criterion(y_pred, y_train)  # 计算损失
    optimizer.zero_grad()  # 在反向传播算法之前  将优化器的梯度设置为0
    loss.backward()  # 反向传播计算参数梯度
    optimizer.step()  # 在优化器中更新参数,不同优化器更新方式不同,但是调用方式相同

print("Parameters:")
for name, para in model.named_parameters():
    print(name, para)

y_pred = model(x_train)
print("Predicted results:", y_pred.argmax(axis=1))

结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值