- 多模態大模型:算法、應用與微調
- 劉兆峰
- 2239字
- 2024-09-11 17:37:22
1.1.3 注意力機制
Seq2Seq結構的模型將輸入序列的信息都壓縮到上下文向量中,而上下文向量的長度是固定的,因此,當輸入序列的長度較長時,一個上下文向量可能存不下那么長的信息,也就是說,此時模型對較長的序列處理能力不足。對此,Bahdanau等人在2014年和Luong等人在2015年提出了一個解決方案——注意力機制。這種機制可以讓模型根據需要來關注輸入序列中的相關部分,從而放大重點位置的信號,因此添加了注意力機制的模型會比沒有添加注意力機制的模型產生更好的結果。
注意力機制在直觀上非常好理解,如圖1-4所示,當我們拿到一張新的圖片時,我們的注意力會自動聚焦到一些關鍵信息上,而不需要掃描全圖。人類的注意力機制能夠減少資源損耗,提高信息處理效率。使用注意力機制的深度學習模型也是如此,能夠更有效地找到圖中的關鍵信息,并給予較高的權重。

圖1-4 注意力機制的圖片演示
帶有注意力機制的Seq2Seq結構模型相比于經典的Seq2Seq結構模型有以下兩點不同。
1)如果有多個編碼器,帶有注意力機制的Seq2Seq結構模型會將更多的中間數據傳遞給解碼器。經典Seq2Seq結構模型只會將編碼階段的最后一個隱藏狀態向量傳遞給解碼器,而帶有注意力機制的Seq2Seq結構模型會將編碼階段的所有隱藏狀態向量傳遞給解碼器,如圖1-5所示。

圖1-5 注意力機制下從多編碼器到解碼器的隱藏狀態傳遞示意
2)解碼器在生成輸出時會執行額外的計算:首先,接收編碼器傳遞的隱藏狀態向量;然后,為每個隱藏狀態向量進行打分;最后,將每個隱藏狀態向量乘以其softmax分數,從而放大高分的隱藏狀態向量。如圖1-6所示。

圖1-6 解碼器利用注意力機制進行計算輸出的示意
為了計算注意力權重,我們可以添加一個前饋層。這個前饋層的輸入是解碼器的輸入和隱藏狀態。由于訓練數據中的句子長度各不相同,我們需要選擇一個最大句子長度作為參考,用于創建和訓練這個前饋層。對于較短的句子,將只使用前幾個注意力權重進行計算,而對于最長的句子,將使用所有的注意力權重。這樣可以確保模型能夠處理不同長度的句子,并且在訓練中學習到如何完成合適的權重分配。
具體的注意力機制的演算過程我們將在1.2節介紹Transformer模型架構時詳細說明,此處我們先給出一個簡單的代碼實現。
python
class Attention(nn.Module):
def __init__(self, hidden_size):
super(Attention, self).__init__()
self.Wa=nn.Linear(hidden_size, hidden_size)
self.Ua=nn.Linear(hidden_size, hidden_size)
self.Va=nn.Linear(hidden_size, 1)
def forward(self, query, keys):
scores=self.Va(th.tanh(self.Wa(query)+self.Ua(keys)))
scores=scores.squeeze(2).unsqueeze(1)
weights=F.softmax(scores, dim=-1)
context=th.bmm(weights, keys)
return context, weights
這段代碼定義了一個Attention類,用于實現注意力機制的功能。在模塊的初始化方法中,通過如下三個線性層(nn.Linear)來定義模塊的參數。
?self.Wa的輸入大小為hidden_size,輸出大小也為hidden_size。
?self.Ua的輸入大小為hidden_size,輸出大小也為hidden_size。
?self.Va的輸入大小為hidden_size,輸出大小為1。
在前向傳播過程中,模塊會使用兩個輸入參數:query和keys。query是一個用于查詢的向量,而keys是一個包含多個用于比較的向量的集合。模塊首先將輸入的query和keys經過線性變換,然后經過tanh激活函數,再相加以產生分數(scores)。分數用來衡量query和每個key之間的相關性。接著,對scores進行squeeze和unsqueeze操作,將其維度從(1, N, 1)變為(1, 1, N),其中N為key的數量。然后,通過softmax函數對scores進行標準化操作,得到每個key的權重(weights)。最后,使用權重對keys進行加權求和(torch.bmm),得到一個加權的上下文向量(context),并將其與權重一起返回。
python
class AttentionDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1):
super(AttentionDecoderRNN, self).__init__()
self.embedding=nn.Embedding(output_size, hidden_size)
self.attention=Attention(hidden_size)
self.rnn=nn.RNN(2 * hidden_size, hidden_size, batch_first=True)
self.out=nn.Linear(hidden_size, output_size)
self.dropout=nn.Dropout(dropout_p)
def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):
batch_size=encoder_outputs.size(0)
decoder_input=th.empty(batch_size, 1, dtype=th.long).fill_(SOS_token)
decoder_hidden=encoder_hidden
decoder_outputs=[]
attentions=[]
for i in range(MAX_LENGTH):
decoder_output, decoder_hidden, attn_weights=self.forward_step(decoder_input, decoder_hidden, encoder_outputs)
decoder_outputs.append(decoder_output)
attentions.append(attn_weights)
if target_tensor is not None: # 強制學習
decoder_input=target_tensor[:, i].unsqueeze(1)
else:
_, topi=decoder_output.topk(1)
decoder_input=topi.squeeze(-1).detach()
decoder_outputs=th.cat(decoder_outputs, dim=1)
decoder_outputs=F.log_softmax(decoder_outputs, dim=-1)
attentions=th.cat(attentions, dim=1)
return decoder_outputs, decoder_hidden, attentions
def forward_step(self, input, hidden, encoder_outputs):
embedded= self.dropout(self.embedding(input))
query=hidden.permute(1, 0, 2)
context, attn_weights=self.attention(query, encoder_outputs)
input_rnn=th.cat((embedded, context), dim=2)
output, hidden=self.rnn(input_rnn, hidden)
output=self.out(output)
return output, hidden, attn_weights
這段代碼定義了一個名為AttentionDecoderRNN的類,用于實現帶有注意力機制的解碼器。在模塊的初始化方法中,定義了幾個子模塊。
?self.embedding是一個嵌入層,用于將輸入序列轉化為嵌入向量。它的輸入大小為output_size(目標語言的詞表大小),輸出大小為hidden_size(隱藏狀態的維度)。
?self.attention是一個注意力模塊,用于實現注意力機制。它的輸入維度為hidden_size(隱藏狀態維度),后面會將編碼器的輸出和隱藏狀態拼接在一起作為解碼器RNN的輸入,因此輸入維度翻倍,為2 * hidden_size。
?self.rnn是一個卷積神經網絡,用于處理輸入序列。它的輸入維度為2 * hidden_size(嵌入向量和上下文向量的拼接),輸出維度為hidden_size。
?self.out是一個線性層,用于將解碼器的輸出映射到最終目標語言的詞表大小。它的輸入維度為hidden_size,輸出維度為output_size。
?self.dropout是一個dropout層,用于防止過擬合,可以在訓練過程中隨機將一些節點置為0。
在前向傳播過程中,輸入參數為encoder_outputs(編碼器輸出)、encoder_hidden(編碼器隱藏狀態)和target_tensor(目標序列)。首先,通過encoder_outputs的形狀獲取batch_size。然后,初始化解碼器的輸入為一個大小為(batch_size, 1)的LongTensor,并用起始標記填充。隱藏狀態起始為編碼器的隱藏狀態。
循環進行解碼的過程,迭代次數為MAX_LENGTH(最大解碼長度)。在每個時間步中,調用forward_step方法完成一步解碼操作。將解碼器的輸出、注意力權重和上一時間步的解碼器輸入存儲到decoder_outputs和attentions列表中。
將decoder_outputs和attentions分別連接(cat)在一起,并對decoder_outputs進行log_softmax操作。該模塊的輸出包括decoder_outputs(解碼器輸出)、decoder_hidden(解碼器隱藏狀態)和attentions(每個時間步的注意力權重)。
在forward_step方法中,輸入參數為input(解碼器輸入)、hidden(隱藏狀態)和encoder_outputs(編碼器輸出)。首先,通過嵌入層對input進行嵌入和dropout操作。然后,對隱藏狀態進行維度轉換,以適應注意力模塊的輸入要求。通過調用attention模塊,獲取上下文向量(context)和注意力權重(attn_weights)。將嵌入向量和上下文向量沿第三個維度(維度索引為2)進行拼接,作為RNN層的輸入。使用RNN層對輸入進行處理從而得到輸出(output)和隱藏狀態(hidden)。最后,通過線性層將輸出映射到目標語言的詞表大小,得到最終的解碼器輸出。
下面我們實例化一個帶注意力機制的解碼器對象,然后使用玩具數據看一下最終輸出向量和注意力權重的維度。
python
decoder=AttentionDecoderRNN(hidden_size=5, output_size=10)
target_vector=th.tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
encoder_outputs, encoder_hidden=encoder(input_vector)
output, hidden, attentions=decoder(encoder_outputs, encoder_hidden, input_vector)
print("輸出向量的維度:", output.size()) # 輸出向量的維度: torch.Size([1, 10, 10])
print("注意力權重的維度:", attentions.size()) # 注意力權重的維度: torch.Size([1, 10, 10])