1. 程式人生 > 其它 >NLP文字分類學習筆記5:帶attention的文字分類

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([email protected]_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拼接後乘以一個引數,作為最後的輸出