NLP文本分类学习笔记5:带attention的文本分类
本节内容有些抽象,自己也可能理解不到位,可能有些错误,请批判性参考
seq2seq
分为encoder和decoder两部分,如下图所示,每一个部分可以使用CNN,RNN,LSTM等模型,输入2针对不同情况可有可无,模型在翻译,文本摘要生成等方面有广泛应用。
在编码器encoder中可以对输入内容编码,表示为一个特征输出,然后输入到解码器decoder中,对特征进行解码产生输出,如以下翻译的例子,输入encoder中“我喜欢梨”,在decoder中进行翻译
翻译的效果全部依赖于encoder部分最后的输出向量。但是,一方面,翻译过程并不全部依赖于全面所有的内容,例如,对于“like”的翻译,对于前面“喜欢”这一词依赖程度更大。另一方面,最后的输出信息保留句子后面的信息多,保留前面的信息较少。所以提出attention注意力机制。
注意力机制attention
有点抽象,自己也迷迷糊糊,先试着主观说一说大致思想:
注意力机制就是要关注重要的信息。
重要的信息如何被关注?就是重要的信息的权重要大一些
权重又怎么来?将所有的信息与参数计算后(key)和输入的内容(query)进行比较,哪个信息和输入的内容相关(相似),哪这个信息权重就要大,因此采用一些计算相似度的函数(两个向量的点积等)来计算(甚至训练一个网络),
最后按权重将这些信息(value)相乘再相加,就是最后的输出
以下图为例(结构并不止这一种),“我喜欢梨”这句话经过encoder训练得到了输出\(h_1\),\(h_2\),\(h_3\),在encoder,start同样得到一个输出\(S_1\)(query),它就和\(h_1\),\(h_2\),\(h_3\)与参数计算后的结果(key)分别计算相似性(这里使用了向量点积),将计算结果归一化处理后,就得到了各自的权重\(w_1\),\(w_2\),\(w_3\),各自的权重与\(h_1\),\(h_2\),\(h_3\)(value)相乘相加后得到\(h'\),与\(s_1\)相乘作为下一时刻decoder的输入。再重复之上的操作。
带attention机制的文本分类
在NLP文本分类学习笔记4:基于RNN的文本分类中,介绍了使用LSTM分类时,使用的是模型最后的输出
在上一节也介绍了只使用最后一个输出可能存在的问题
所以可以使用attention机制,对LSTM所有的输出进行加权求和来作为最后的输出,attention机制实际上就是求一个这样的权重。
pytorch实现基于LSTM带attention的文本分类
借助NLP文本分类学习笔记4中LSTM模型,加入attention机制,实现文本分类。网络结构如下所示,详细代码见NLP文本分类学习笔记0。
将LSTM所有时刻的输出(key)与随机参数(q)相乘再加一个偏置单元,并经过tanh得到权重
权重经过softmax归一化后作为权重,与将LSTM所有时刻的输出(key)相乘再相加。
把这个带权重的值作为LSTM最后的输出来分类(在NLP文本分类学习笔记4中,是用最后的输出分类)
最后在测试集上的准确率为86.79%
在Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification这篇论文中作者使用的注意力机制为
也就是将LSTM所有时刻的输出H经过tanh函数运算后,与初始化的随机参数W(w大小与词向量维度相同)相乘后归一化,最后再与H相乘,并再次经过tanh运算,代码为下述代码中注释的部分
最后在测试集上的准确率为87.26%
如下为两种机制的实现
import json
import pickle
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class Config(object):
def __init__(self, embedding_pre):
self.embedding_path = 'data/embedding.npz'
self.embedding_model_path = "mymodel/word2vec.model"
self.train_path = 'data/train.df' # 训练集
self.dev_path = 'data/valid.df' # 验证集
self.test_path = 'data/test.df' # 测试集
self.class_path = 'data/class.json' # 类别名单
self.vocab_path = 'data/vocab.pkl' # 词表
self.save_path ='mymodel/attention.pth' # 模型训练结果
self.embedding_pretrained = torch.tensor(np.load(self.embedding_path, allow_pickle=True)["embeddings"].astype(
'float32')) if embedding_pre == True else None # 预训练词向量
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
self.dropout = 0.5 # 随机失活
self.num_classes = len(json.load(open(self.class_path, encoding='utf-8'))) # 类别数
self.n_vocab = 0 # 词表大小,在运行时赋值
self.epochs = 10 # epoch数
self.batch_size = 128 # mini-batch大小
self.maxlen = 32 # 每句话处理成的长度(短填长切)
self.learning_rate = 1e-3 # 学习率
self.embed_size = self.embedding_pretrained.size(1) \
if self.embedding_pretrained is not None else 200 # 字向量维度
self.hidden_size = 128 # lstm隐藏层
self.num_layers = 2 # lstm层数
class myAttention(nn.Module):
def __init__(self,input_size):
super(myAttention,self).__init__()
self.input_size=input_size
self.word_weight=nn.Parameter(torch.Tensor(self.input_size))
self.word_bias=nn.Parameter(torch.Tensor(1))
self._create_weights()
def _create_weights(self,mean=0.0,std=0.05):
self.word_weight.data.normal_(mean,std)
self.word_bias.data.normal_(mean,std)
def forward(self,inputs):
att=torch.einsum('abc,c->ab',(inputs,self.word_weight))+self.word_bias
att=torch.tanh(att)
att=F.softmax(att,dim=1)
att=torch.einsum('abc,ab->ac',(inputs,att))
# #论文中的机制
# att=torch.tanh(inputs)
# att=F.softmax(att@self.word_weight,dim=1).unsqueeze(-1)
# att=torch.sum(inputs*att,1)
# att=torch.tanh(att)
return att
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
vocab = pickle.load(open(config.vocab_path, 'rb'))
config.n_vocab=len(vocab.dict)
self.embedding = nn.Embedding(config.n_vocab, config.embed_size, padding_idx=config.n_vocab - 1)
self.lstm = nn.LSTM(config.embed_size, config.hidden_size, config.num_layers,
bidirectional=True, batch_first=True, dropout=config.dropout)
self.att=myAttention(config.hidden_size*2)
self.fc = nn.Linear(config.hidden_size * 2, config.num_classes)
def forward(self, x):
emb = self.embedding(x)
out, _ = self.lstm(emb)
out=self.att(out)
out = self.fc(out)
return out
自注意力机制self-attention
参考:
https://www.bilibili.com/video/BV1Wv411h7kN?p=38
https://www.cnblogs.com/erable/p/15072941.html
注意力机制是对于目标来提取输入中重要的信息,而自注意力机制是提取输入序列元素间重要的信息,对于一个序列它打破了序列间距离的限制,能够提取到较远的两个输入之间的关系而不用担心距离造成的影响。在文本分类中,使用一层self-attention,可以捕捉序列中任意两词之间的信息。也可以多叠加几层,层之间使用全连接层连接。
对于一个序列\(A\),不需要另外的条件,可以自己得到一个序列\(B\)。如下图所示,对于序列中每个元素\(a\),分别与三个参数矩阵相乘得到q,k和v,用q和其它元素的k相乘可以得到该元素与其它元素的相似度,也就是权重,权重经过归一化(softmax,图中未画出,也就是对\(α'_{1,1}\),\(α'_{1,2}\)等归一化),再与各个元素的v相乘,再相加就得到了第一个输出(图中的\(b_1\)),对于\(b_2\)等也同样方法求出
pytorch实现基于LSTM带自注意力机制的文本分类
这里将自注意力机制单独拿出来,加到单词嵌入层之后,其主要思想是能够捕捉到到两个距离远的词之间的联系,网络结构如下所示,也就是对上述机制的简单实现,其中对于“对于序列中每个元素\(a\),分别与三个参数矩阵相乘得到q,k和v”这一过程,直接使用了全连接层nn.Linear来计算,因为其本质就是输入与随机参数矩阵的相乘。最后将o作为LSTM模型的输入
最后在测试集上的准确率为85.35%
import json
import pickle
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class Config(object):
def __init__(self, embedding_pre):
self.embedding_path = 'data/embedding.npz'
self.embedding_model_path = "mymodel/word2vec.model"
self.train_path = 'data/train.df' # 训练集
self.dev_path = 'data/valid.df' # 验证集
self.test_path = 'data/test.df' # 测试集
self.class_path = 'data/class.json' # 类别名单
self.vocab_path = 'data/vocab.pkl' # 词表
self.save_path ='mymodel/selfattention.pth' # 模型训练结果
self.embedding_pretrained = torch.tensor(np.load(self.embedding_path, allow_pickle=True)["embeddings"].astype(
'float32')) if embedding_pre == True else None # 预训练词向量
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
self.dropout = 0.5 # 随机失活
self.num_classes = len(json.load(open(self.class_path, encoding='utf-8'))) # 类别数
self.n_vocab = 0 # 词表大小,在运行时赋值
self.epochs = 10 # epoch数
self.batch_size = 128 # mini-batch大小
self.maxlen = 32 # 每句话处理成的长度(短填长切)
self.learning_rate = 1e-3 # 学习率
self.embed_size = self.embedding_pretrained.size(1) \
if self.embedding_pretrained is not None else 200 # 字向量维度
self.hidden_size = 128 # lstm隐藏层
self.num_layers = 2 # lstm层数
class mySelfAttention(nn.Module):
def __init__(self,config):
super(mySelfAttention, self).__init__()
self.WQ=nn.Linear(config.embed_size,config.embed_size,bias=False)
self.WK = nn.Linear(config.embed_size,config.embed_size,bias=False)
self.WV = nn.Linear(config.embed_size,config.embed_size,bias=False)
def forward(self,inputs):
Q=self.WQ(inputs)
K = self.WQ(inputs).permute(0,2,1)
V = self.WQ(inputs)
a=F.softmax(Q@K,dim=1)
o=a@V
return o
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
vocab = pickle.load(open(config.vocab_path, 'rb'))
config.n_vocab=len(vocab.dict)
self.embedding = nn.Embedding(config.n_vocab, config.embed_size, padding_idx=config.n_vocab - 1)
self.selfatt=mySelfAttention(config)
self.lstm = nn.LSTM(config.embed_size, config.hidden_size, config.num_layers,
bidirectional=True, batch_first=True, dropout=config.dropout)
self.fc = nn.Linear(config.hidden_size * 2, config.num_classes)
def forward(self, x):
emb = self.embedding(x)
selfatt=self.selfatt(emb)
out, _ = self.lstm(selfatt)
out = self.fc(out[:,-1,:])
return out
多头注意力机制multi-head attention
基于单词之间关系可能不止一种的思想,使用多组QKV,分别捕捉不同的关系,如下图所示,以两个头为例,在原有的QKV基础上在乘以参数矩阵,得到两组QKV,之后分别得到两个输出b,将b拼接后乘以一个参数,作为最后的输出