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这篇论文中作者使用的注意力机制为

\[M=tanh(H) \]

\[α=softmax(w^TM) \]

\[r=Hα^T \]

\[h^*=tanh(r) \]

也就是将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拼接后乘以一个参数,作为最后的输出

posted @ 2022-04-08 10:31  启林O_o  阅读(717)  评论(0编辑  收藏  举报