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\)
帶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([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拼接後乘以一個引數,作為最後的輸出