dreamweaver做網(wǎng)站學習解析做百度推廣員賺錢嗎
一、前言
在自然語言處理(NLP)領域,模型架構的不斷發(fā)展極大地推動了技術的進步。從早期的循環(huán)神經(jīng)網(wǎng)絡(RNN)到長短期記憶網(wǎng)絡(LSTM)、Transformer再到當下火熱的Mamba(放在下一節(jié)),每一種架構都帶來了不同的突破和應用。本文將詳細介紹這些經(jīng)典的模型架構及其在PyTorch中的實現(xiàn),由于我只是門外漢(想擴展一下知識面),如果有理解不到位的地方歡迎評論指正~。
個人感覺NLP的任務本質上是一個序列到序列的過程,給定輸入序列 ,要通過一個函數(shù)實現(xiàn)映射,得到輸出序列
,這里的x1、x2、x3可以理解為一個個單詞,NLP的具體應用有:
-
機器翻譯:將源語言的句子(序列)翻譯成目標語言的句子(序列)。
-
文本生成:根據(jù)輸入序列生成相關的輸出文本,如文章生成、對話生成等。
-
語音識別:將語音信號(序列)轉換為文本(序列)。
-
文本分類:盡管最終輸出是一個類別標簽,但在一些高級應用中,也可以將其看作是將文本序列映射到某個特定的輸出序列(如標簽序列)。
二、RNN和LSTM
2.1 RNN
循環(huán)神經(jīng)網(wǎng)絡(RNN)是一種適合處理序列數(shù)據(jù)的神經(jīng)網(wǎng)絡架構。與傳統(tǒng)的前饋神經(jīng)網(wǎng)絡(線性層)不同,RNN具有循環(huán)連接,能夠在序列數(shù)據(jù)的處理過程中保留和利用之前的狀態(tài)信息。網(wǎng)絡結構如下所示:
RNN的網(wǎng)絡結構
x和隱藏狀態(tài)h的計算過程
RNN通過在網(wǎng)絡中引入循環(huán)連接,將前一個時間步的輸出作為當前時間步的輸入之一,使得網(wǎng)絡能夠記住以前的狀態(tài)。具體來說,RNN的每個時間步都會接收當前輸入和前一個時間步的隱藏狀態(tài),并輸出新的隱藏狀態(tài)。其核心公式為:
-
𝑥𝑡 是當前時間步的輸入。
-
?𝑡 是當前時間步的隱藏狀態(tài)。
-
?𝑡?1 是前一個時間步的隱藏狀態(tài)(如果是第一個輸入,這項是0)。
-
𝑦𝑡 是當前時間步的輸出。
-
𝑊?𝑥𝑊??𝑊?𝑦 都是權重矩陣,是可以共享參數(shù)的。
-
𝑏? 𝑏𝑦 是偏置。
-
𝜎𝜙 是激活函數(shù)。
2.1.1 RNN的優(yōu)點
-
處理序列數(shù)據(jù):RNN可以處理任意長度的序列數(shù)據(jù),并能夠記住序列中的上下文信息。
-
參數(shù)共享:RNN在不同時間步之間共享參數(shù),使得模型在處理不同長度的序列時更加高效。
2.1.2 RNN的缺點
-
梯度消失和爆炸:在訓練過程中,RNN會遇到梯度消失和梯度爆炸的問題,導致模型難以訓練或收斂緩慢。
-
長距離依賴問題:RNN在處理長序列數(shù)據(jù)時,容易遺忘較早的上下文信息,難以捕捉長距離依賴關系。
-
不能并行訓練:每個時間步的計算需要依賴于前一個時間步的結果,這導致RNN的計算不能完全并行化,必須按順序進行。這種順序性限制了RNN的訓練速度,但是推理不受影響(盡管推理過程同樣受到順序性依賴的限制,但相比訓練過程,推理的計算量相對較小,因為推理時不需要進行反向傳播和梯度計算。推理過程主要集中在前向傳播,即根據(jù)輸入數(shù)據(jù)通過模型得到輸出。因此,推理過程中的計算相對較快,盡管不能并行化,但在許多實際應用中仍然可以達到實時或接近實時的性能)。
關于長距離依賴問題的理解:
在RNN中,每個時間步的信息會被傳遞到下一個時間步。然而,隨著序列長度的增加,早期時間步的信息需要通過許多次的傳遞才能影響到后續(xù)時間步。每次傳遞過程中,信息可能會逐漸衰減。這種逐步衰減導致RNN在處理長序列數(shù)據(jù)時,早期時間步的信息可能被遺忘或淹沒在新信息中。
同時,在訓練RNN時,通過時間反向傳播算法(Backpropagation Through Time, BPTT)來更新參數(shù)。這一過程中,梯度會從輸出層反向傳播到輸入層。然而,長序列中的梯度在多次反向傳播時,容易出現(xiàn)梯度消失(梯度逐漸變小,趨近于零)或梯度爆炸(梯度過大,導致數(shù)值不穩(wěn)定)的現(xiàn)象。梯度消失會導致模型難以學習和記住長距離依賴的信息,梯度爆炸則會導致模型參數(shù)更新不穩(wěn)定。
2.1.3 代碼實現(xiàn)
以下的實現(xiàn)都是基于文本分類任務進行的:
import torch
import torch.nn as nnclass TextRNN(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers, dropout, num_classes):super(TextRNN, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.rnn = nn.RNN(embedding_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout)self.fc = nn.Linear(hidden_dim, num_classes)self.dropout = nn.Dropout(dropout)def forward(self, x):x = self.embedding(x)rnn_out, hidden = self.rnn(x)x = self.dropout(rnn_out[:, -1, :])x = self.fc(x)return x
如果不用torch自帶RNN的api的話,下面是自搭版本:
import torch
import torch.nn as nnclass CustomRNNLayer(nn.Module):def __init__(self, input_dim, hidden_dim):super(CustomRNNLayer, self).__init__()self.hidden_dim = hidden_dimself.i2h = nn.Linear(input_dim + hidden_dim, hidden_dim)self.h2o = nn.Linear(hidden_dim, hidden_dim)self.tanh = nn.Tanh()def forward(self, input, hidden):combined = torch.cat((input, hidden), 1)hidden = self.tanh(self.i2h(combined))output = self.h2o(hidden)return output, hiddenclass TextRNN(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers, dropout, num_classes):super(TextRNN, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.hidden_dim = hidden_dimself.num_layers = num_layersself.rnn1 = CustomRNNLayer(embedding_dim, hidden_dim)self.rnn2 = CustomRNNLayer(hidden_dim, hidden_dim)self.dropout = nn.Dropout(dropout)self.fc = nn.Linear(hidden_dim, num_classes)def forward(self, x):x = self.embedding(x)batch_size, seq_len, _ = x.shapehidden1 = torch.zeros(batch_size, self.hidden_dim).to(x.device)hidden2 = torch.zeros(batch_size, self.hidden_dim).to(x.device)for t in range(seq_len):input_t = x[:, t, :]hidden1, _ = self.rnn1(input_t, hidden1)hidden2, _ = self.rnn2(hidden1, hidden2)x = self.dropout(hidden2)x = self.fc(x)return x
初始化 hidden1 和 hidden2 為零張量,表示第一個和第二個RNN層的初始隱藏狀態(tài),遍歷序列長度 seq_len 的每個時間步,將當前時間步的輸入向量 input_t 輸入到第一個RNN層,更新 hidden1;再將 hidden1 輸入到第二個RNN層,更新 hidden2。
特別解釋一下,input_t = x[:, t, :] 是提取當前時間步 t 的輸入向量,原本的x是(batch_size, seq_len, embedding_dim),處理后是(batch_size, embedding_dim)。
2.2 LSTM
長短期記憶網(wǎng)絡(LSTM)是一種特殊的循環(huán)神經(jīng)網(wǎng)絡(RNN)架構,旨在解決傳統(tǒng)RNN在處理長序列數(shù)據(jù)時的梯度消失和梯度爆炸問題。LSTM通過引入記憶單元(cell state)和門控機制(gate mechanism),能夠更好地捕捉和保留長距離依賴關系。
LSTM的基本結構包括一個記憶單元和三個門:輸入門、遺忘門和輸出門。這些門用于控制信息在LSTM單元中的流動。LSTM的工作原理可以用以下步驟描述:
-
遺忘門(Forget Gate):決定記憶單元中的哪些信息需要被遺忘。
-
輸入門(Input Gate):決定哪些新信息需要被存儲到記憶單元中。
-
輸出門(Output Gate):決定記憶單元中的哪些信息需要輸出。
LSTM的網(wǎng)絡結構,可以看到和RNN相似,但是用到門控和記憶機制
LSTM在每個時間步的計算可以分為以下4個階段,也對應了上圖出現(xiàn)的順序:
遺忘門的計算:
遺忘門 ft決定了前一個時間步的記憶單元狀態(tài)C t-1 中哪些信息需要被遺忘。 σ是 sigmoid 激活函數(shù)(輸出限制在 [0, 1] 之間,0就代表了遺忘,不許任何量通過,1就指允許任意量通過,從而使得網(wǎng)絡就能了解哪些數(shù)據(jù)是需要遺忘,哪些數(shù)據(jù)是需要保存), wf是遺忘門的權重矩陣,bf是偏置。 這是一個concat連接操作。
輸入門的計算:
輸入門 it決定了當前輸入xt中哪些信息需要被添加到記憶單元中, Ct是新的候選記憶, Wi和Wc 分別是輸入門和候選記憶的權重矩陣,bi和bc 是偏置。
tanh激活函數(shù)的范圍是-1~1,它對新信息進行變換,使得新信息能夠取正值和負值。這樣可以更靈活地調整記憶單元狀態(tài),從而保留和更新信息
更新記憶單元狀態(tài):
記憶單元狀態(tài)Ct通過遺忘門和輸入門的輸出進行更新,融合了前一個時間步的記憶和當前時間步的新信息。
輸出門的計算:
輸出門 ot 決定了記憶單元中哪些信息需要輸出,最終的隱藏狀態(tài) ht?通過記憶單元狀態(tài) Ct? 以及輸出門的控制生成。
單個計算模塊的細節(jié)
2.2.1 LSTM的優(yōu)點
-
解決長距離依賴問題:LSTM通過引入記憶單元(cell state)和門控機制(遺忘門、輸入門和輸出門),有效地解決了傳統(tǒng)RNN的長距離依賴問題。它能夠記住長時間跨度內的重要信息,避免了信息在多次傳遞逐漸衰減。
-
緩解梯度消失和梯度爆炸問題:在傳統(tǒng)RNN中,梯度消失和梯度爆炸是常見的問題,特別是在處理長序列時。LSTM通過其門控機制,能夠更穩(wěn)定地傳遞梯度,減少了梯度消失和爆炸的發(fā)生,從而提高了訓練效果。
-
靈活的記憶更新:LSTM的記憶單元和門控機制使得網(wǎng)絡能夠有選擇性地記住和遺忘信息。這種靈活性使得LSTM在處理復雜的時間序列數(shù)據(jù)時表現(xiàn)出色,能夠捕捉到數(shù)據(jù)中的重要模式和特征。
2.2.2 LSTM的缺點
-
計算復雜度高:相較于簡單的RNN,LSTM的結構更復雜,包含更多的參數(shù)(如多個門和記憶單元)。這種復雜性增加了計算成本,導致訓練和推理速度較慢。
-
難以并行化:LSTM的順序計算特性限制了其并行化的能力。在處理長序列時,每個時間步的計算依賴于前一個時間步的結果,這使得計算不能完全并行化,從而影響訓練和推理的效率。
-
對長序列仍有局限:盡管LSTM在處理長距離依賴問題上比傳統(tǒng)RNN有顯著改善,但在極長序列的情況下,仍可能遇到信息遺忘和梯度衰減的問題。
2.2.3 代碼實現(xiàn)
import torch
import torch.nn as nnclass TextLSTM(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers, dropout, num_classes):super(TextLSTM, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout)self.dropout = nn.Dropout(dropout)self.fc = nn.Linear(hidden_dim, num_classes)def forward(self, x):x = self.embedding(x)batch_size, seq_len, _ = x.shapeh_0 = torch.zeros(self.lstm.num_layers, batch_size, self.lstm.hidden_size).to(x.device)c_0 = torch.zeros(self.lstm.num_layers, batch_size, self.lstm.hidden_size).to(x.device)x, (h_n, c_n) = self.lstm(x, (h_0, c_0))x = self.dropout(h_n[-1])x = self.fc(x)return x
自搭版本:
import torch
import torch.nn as nnclass CustomLSTMLayer(nn.Module):def __init__(self, input_dim, hidden_dim):super(CustomLSTMLayer, self).__init__()self.hidden_dim = hidden_dimself.i2f = nn.Linear(input_dim + hidden_dim, hidden_dim)self.i2i = nn.Linear(input_dim + hidden_dim, hidden_dim)self.i2c = nn.Linear(input_dim + hidden_dim, hidden_dim)self.i2o = nn.Linear(input_dim + hidden_dim, hidden_dim)self.tanh = nn.Tanh()self.sigmoid = nn.Sigmoid()def forward(self, input, hidden, cell):combined = torch.cat((input, hidden), 1)f_t = self.sigmoid(self.i2f(combined))i_t = self.sigmoid(self.i2i(combined))c_tilde_t = self.tanh(self.i2c(combined))c_t = f_t * cell + i_t * c_tilde_to_t = self.sigmoid(self.i2o(combined))h_t = o_t * self.tanh(c_t)return h_t, c_tclass TextLSTM(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers, dropout, num_classes):super(TextLSTM, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.hidden_dim = hidden_dimself.num_layers = num_layersself.lstm1 = CustomLSTMLayer(embedding_dim, hidden_dim)self.lstm2 = CustomLSTMLayer(hidden_dim, hidden_dim)self.dropout = nn.Dropout(dropout)self.fc = nn.Linear(hidden_dim, num_classes)def forward(self, x):x = self.embedding(x)batch_size, seq_len, _ = x.shapehidden1 = torch.zeros(batch_size, self.hidden_dim).to(x.device)cell1 = torch.zeros(batch_size, self.hidden_dim).to(x.device)hidden2 = torch.zeros(batch_size, self.hidden_dim).to(x.device)cell2 = torch.zeros(batch_size, self.hidden_dim).to(x.device)for t in range(seq_len):input_t = x[:, t, :]hidden1, cell1 = self.lstm1(input_t, hidden1, cell1)hidden2, cell2 = self.lstm2(hidden1, hidden2, cell2)x = self.dropout(hidden2)x = self.fc(x)return x
三、TextCNN
TextCNN(文本卷積神經(jīng)網(wǎng)絡)是一種應用于自然語言處理(NLP)任務的卷積神經(jīng)網(wǎng)絡(CNN)模型。
TextCNN的基本結構包括以下幾個部分:
-
嵌入層(Embedding Layer):將輸入的文本序列轉換為稠密的詞向量表示。這些詞向量可以是預訓練的詞向量(如Word2Vec、GloVe)或在訓練過程中學習到的嵌入。
-
卷積層(Convolutional Layer):對嵌入后的詞向量序列應用卷積操作,提取不同大小的n-gram特征。卷積核的大小可以設定為不同的窗口大小(如2, 3, 4等),以捕捉不同范圍的局部特征。
-
池化層(Pooling Layer):對卷積后的特征圖應用最大池化操作,將每個卷積核的輸出縮減為一個固定大小的特征向量。這一步有助于提取最重要的特征,并減少特征的維度。
-
全連接層(Fully Connected Layer):將池化后的特征向量連接成一個長的特征向量,輸入到全連接層中進行分類。最后一層通常是一個Softmax層,用于輸出分類結果。
具體流程如下:
-
輸入文本:輸入一個文本序列,假設長度為n,每個詞通過詞匯表索引轉換為詞向量表示,形成一個形狀為(n,d)的矩陣,其中 d 是詞向量的維度。
-
卷積操作:使用不同大小的卷積核(如2, 3, 4)對嵌入矩陣進行卷積操作,提取不同n-gram的局部特征。卷積后的特征圖形狀為(n-k+1, m),其中 k 是卷積核的大小,m 是卷積核的數(shù)量。
-
最大池化:對每個卷積核的輸出特征圖進行最大池化操作,提取重要的特征。池化后的特征向量形狀為 (m, )。
-
特征拼接:將不同卷積核和池化操作得到的特征向量拼接成一個長的特征向量,輸入到全連接層。
-
分類輸出:最后通過全連接層和Softmax層進行分類,輸出各類別的概率。
TextCNN的網(wǎng)絡結構
3.1 TextCNN的優(yōu)點
-
高效提取局部特征:卷積操作能夠有效提取不同n-gram范圍內的局部特征,對于捕捉文本的局部模式非常有效。
-
并行計算:卷積操作和池化操作可以并行計算,相對于RNN等順序模型,訓練和推理速度更快。
3.2 TextCNN的缺點
-
缺乏長距離依賴:由于卷積操作的感受野有限,TextCNN在捕捉長距離依賴方面不如LSTM等序列模型表現(xiàn)好。
-
固定大小的卷積核:雖然可以通過多種卷積核來捕捉不同的n-gram特征,但仍然受限于卷積核的大小,對于變長依賴的建模能力有限。
3.3 權值共享
權值共享是指在卷積神經(jīng)網(wǎng)絡的卷積操作中,同一卷積核(filter)的參數(shù)在整個輸入圖像或特征圖上被重復使用。這意味著,對于一個卷積層中的每一個卷積核,其參數(shù)在整個輸入圖像的不同位置上是相同的。
-
降低參數(shù):在傳統(tǒng)的全連接層中,每個神經(jīng)元都有自己的權重參數(shù),輸入維度較大時會導致參數(shù)數(shù)量龐大。而在卷積層中,由于使用了權值共享,一個卷積核的參數(shù)數(shù)量固定,獨立于輸入圖像的大小。這大大減少了模型的參數(shù)數(shù)量。
-
提升訓練效率:由于參數(shù)數(shù)量減少,權值共享使得模型訓練變得更加高效。需要學習的參數(shù)變少了,相應的訓練時間也減少了。
-
空間不變性(Translation Invariance):權值共享意味著卷積核在輸入圖像的不同位置都使用相同的參數(shù),這使得卷積神經(jīng)網(wǎng)絡可以識別圖像中的特征,不管這些特征出現(xiàn)在圖像的哪個位置。這樣,模型可以更好地處理位移和變形,提高對輸入圖像的泛化能力。
3.4 代碼實現(xiàn)
import torch
import torch.nn as nn
import torch.nn.functional as Fclass TextCNN(nn.Module):def __init__(self, vocab_size, embedding_dim, num_filters, kernel_sizes, dropout, num_classes):super(TextCNN, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.conv1 = nn.Conv2d(1, num_filters, (kernel_sizes[0], embedding_dim))self.conv2 = nn.Conv2d(1, num_filters, (kernel_sizes[1], embedding_dim))self.conv3 = nn.Conv2d(1, num_filters, (kernel_sizes[2], embedding_dim))self.dropout = nn.Dropout(dropout)self.fc = nn.Linear(num_filters * len(kernel_sizes), num_classes)def forward(self, x):x = self.embedding(x)x = x.unsqueeze(1) # 增加通道維度方便卷積處理conv1_out = F.relu(self.conv1(x)).squeeze(3)pooled1 = F.max_pool1d(conv1_out, conv1_out.size(2)).squeeze(2)conv2_out = F.relu(self.conv2(x)).squeeze(3)pooled2 = F.max_pool1d(conv2_out, conv2_out.size(2)).squeeze(2)conv3_out = F.relu(self.conv3(x)).squeeze(3)pooled3 = F.max_pool1d(conv3_out, conv3_out.size(2)).squeeze(2)x = torch.cat((pooled1, pooled2, pooled3), 1)x = self.dropout(x)x = self.fc(x)return x
四、Transformer
Transformer是老熟人了,是目前主流的網(wǎng)絡架構,當然它最早還是起源于NLP領域。
Transformer模型主要由兩個部分組成:編碼器(Encoder)和解碼器(Decoder)。編碼器和解碼器各自由多個相同的層(layer)堆疊而成,每一層包含兩個主要子層(sublayer):
-
編碼器(Encoder):由多個相同的編碼器層堆疊組成,每個編碼器層包含一個自注意力子層和一個前饋神經(jīng)網(wǎng)絡子層。
-
解碼器(Decoder):由多個相同的解碼器層堆疊組成,每個解碼器層包含一個自注意力子層、一個編碼器-解碼器注意力子層和一個前饋神經(jīng)網(wǎng)絡子層。
4.1 自注意力機制(Self-Attention)
自注意力機制是Transformer的核心組件,用于計算輸入序列中每個位置的表示。具體來說,自注意力機制通過計算輸入序列中每個位置與其他所有位置的相似度來捕捉全局依賴關系。計算公式如下:
其中:
-
Q(Query)是查詢矩陣。
-
K(Key)是鍵矩陣。
-
V(Value)是值矩陣。
-
dk? 是鍵向量的維度。
-
其實,QKV都是來自一個x經(jīng)過不同的權重矩陣計算得到的。
4.2 多頭注意力機制(Multi-Head Attention)
為了進一步提升模型的表達能力,Transformer采用了多頭注意力機制。多頭注意力通過對輸入進行多個獨立的自注意力計算(稱為頭),并將結果拼接在一起,通過線性變換生成最終的輸出。公式如下:
其中每個頭的計算為:
4.3 前饋神經(jīng)網(wǎng)絡(Feed-Forward Neural Network)
每個編碼器和解碼器層還包含一個前饋神經(jīng)網(wǎng)絡子層。這個子層包含兩個線性變換和一個激活函數(shù)(通常是ReLU):
4.4 整體流程
Transformer網(wǎng)絡框架
Transformer模型通過嵌入層和位置編碼將輸入序列轉化為稠密向量表示,然后經(jīng)過編碼器和解碼器的多層處理,捕捉序列中的全局依賴關系。
編碼器通過多頭自注意力機制和前饋神經(jīng)網(wǎng)絡提取輸入序列的特征,解碼器通過掩碼多頭自注意力機制(遮住了遮蓋掉未來的時間步,防止解碼器在生成當前時間步的輸出時看到未來的單詞,確保自回歸性質。)、編碼器-解碼器注意力機制和前饋神經(jīng)網(wǎng)絡生成輸出序列。最后通過線性層和Softmax層生成輸出單詞的概率分布。加法和歸一化(Add & Norm,其實就是殘差和LayerNorm)操作在每個子層后確保梯度穩(wěn)定,幫助訓練更深的網(wǎng)絡。
在Transformer模型的解碼器部分,"outputs (shifted right)" 指的是在解碼過程中,模型使用已經(jīng)生成的輸出單詞作為當前時間步的輸入,同時將這些輸出單詞整體向右偏移一個位置,以確保模型生成下一個單詞時只能依賴之前生成的單詞,而不是未來的單詞。
假設要生成一個法語句子 "Je suis étudiant"。具體步驟如下:
編碼器處理
-
編碼器接收英語句子 "I am a student"。
-
編碼器生成全局上下文表示,提供給解碼器。
解碼器生成
-
解碼器首先接收起始標記 <sos> 作為輸入(這里就體現(xiàn)了右移,因為第一個單詞變成了一個特定的符號),生成第一個單詞 "Je"。
-
在生成 "Je" 后,將 "Je" 作為下一個時間步的輸入。解碼器現(xiàn)在的輸入是 <sos> Je,它只能看到 "Je" 之前的內容。
-
解碼器生成第二個單詞 "suis"。接下來,解碼器的輸入是 <sos> Je suis。
-
這一過程不斷重復,解碼器在每個時間步只能看到之前生成的單詞,而不能看到未來的單詞。
多頭注意力機制
將查詢(Q)、鍵(K)和值(V)通過多個線性變換,拆分成多個組(頭),每個頭獨立計算注意力分數(shù)和加權求和值。最后,所有頭的輸出拼接在一起,通過一個線性變換恢復到原來的維度。這種設計允許模型在不同的子空間中關注不同部分的信息,從而提高模型的表達能力和捕捉復雜模式的能力。
多頭注意力機制示意圖
4.5 Transformer的優(yōu)點
-
并行化計算:由于不依賴于前一個時間步的計算結果,Transformer可以并行處理整個序列,這顯著提高了訓練和推理的速度。
-
捕捉全局依賴:自注意力機制能夠捕捉序列中任意兩個位置之間的依賴關系(具體體現(xiàn)在是矩陣運算),特別適合長序列的處理。
-
擴展性強:Transformer架構具有很強的擴展性,可以通過增加層數(shù)和頭數(shù)來提高模型的表達能力。
4.6 Transformer的缺點
-
計算資源消耗大:自注意力機制的計算復雜度為 𝑂(𝑛2?𝑑),其中n是序列長度,d是模型的維度。這使得Transformer在處理非常長的序列時計算資源消耗較大。
-
需要大量數(shù)據(jù):Transformer模型通常需要大量的數(shù)據(jù)來進行訓練,以充分發(fā)揮其性能優(yōu)勢。這在數(shù)據(jù)稀缺的任務中可能成為一個限制因素。主要是在ViT那篇論文中提到了,Transformer結構缺少一些CNN本身設計的歸納偏置(其實就是卷積結構帶來的先驗經(jīng)驗),比如平移不變性和包含局部關系,因此在規(guī)模不足的數(shù)據(jù)集上表現(xiàn)沒有那么好。所以,卷積結構其實是一種trick,而transformer結構是沒有這種trick的,就需要更多的數(shù)據(jù)來讓它學習這種結構。
4.7 Pytorch代碼實現(xiàn)
import torch
import torch.nn as nn
import torch.nn.functional as Fclass Transformer(nn.Module):def __init__(self, src_vocab_size, tgt_vocab_size, d_model, nhead, num_encoder_layers, num_decoder_layers,dim_feedforward, dropout=0.1):super(Transformer, self).__init__()self.d_model = d_model# 定義源語言和目標語言的嵌入層self.src_embedding = nn.Embedding(src_vocab_size, d_model)self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)# 位置編碼層self.pos_encoder = PositionalEncoding(d_model, dropout)# Transformer模型self.transformer = nn.Transformer(d_model, nhead, num_encoder_layers, num_decoder_layers, dim_feedforward,dropout)# 輸出層self.fc_out = nn.Linear(d_model, tgt_vocab_size)def forward(self, src, tgt, src_mask, tgt_mask, src_padding_mask, tgt_padding_mask, memory_key_padding_mask):# 對源輸入進行嵌入和位置編碼src = self.src_embedding(src) * torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))src = self.pos_encoder(src)# 對目標輸入進行嵌入和位置編碼tgt = self.tgt_embedding(tgt) * torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))tgt = self.pos_encoder(tgt)# 編碼器memory = self.transformer.encoder(src, mask=src_mask, src_key_padding_mask=src_padding_mask)# 解碼器output = self.transformer.decoder(tgt, memory, tgt_mask=tgt_mask, memory_mask=None,tgt_key_padding_mask=tgt_padding_mask,memory_key_padding_mask=memory_key_padding_mask)# 輸出層output = self.fc_out(output)return outputclass PositionalEncoding(nn.Module):def __init__(self, d_model, dropout=0.1, max_len=5000):super(PositionalEncoding, self).__init__()self.dropout = nn.Dropout(p=dropout)# 初始化位置編碼矩陣pe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model))pe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)pe = pe.unsqueeze(0).transpose(0, 1)self.register_buffer('pe', pe)def forward(self, x):x = x + self.pe[:x.size(0), :]return self.dropout(x)def generate_square_subsequent_mask(sz):# 生成一個上三角矩陣,防止解碼器看到未來的tokenmask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))return maskdef create_padding_mask(seq):# 創(chuàng)建填充mask,忽略填充部分seq = seq == 0return seq# 使用示例
src_vocab_size = 10000
tgt_vocab_size = 10000
d_model = 512
nhead = 8
num_encoder_layers = 6
num_decoder_layers = 6
dim_feedforward = 2048
dropout = 0.1model = Transformer(src_vocab_size, tgt_vocab_size, d_model, nhead, num_encoder_layers, num_decoder_layers,dim_feedforward, dropout)src = torch.randint(0, src_vocab_size, (10, 32)) # (源序列長度, 批次大小)
tgt = torch.randint(0, tgt_vocab_size, (20, 32)) # (目標序列長度, 批次大小)
src_mask = generate_square_subsequent_mask(src.size(0))
tgt_mask = generate_square_subsequent_mask(tgt.size(0)) # 生成shifted mask,防止解碼器看到未來的token
src_padding_mask = create_padding_mask(src).transpose(0, 1) # 調整mask形狀為 (批次大小, 源序列長度)
tgt_padding_mask = create_padding_mask(tgt).transpose(0, 1) # 調整mask形狀為 (批次大小, 目標序列長度)
memory_key_padding_mask = src_padding_maskoutput = model(src, tgt, src_mask, tgt_mask, src_padding_mask, tgt_padding_mask, memory_key_padding_mask)
print(output.shape) # 應該是 (目標序列長度, 批次大小, 目標詞匯表大小)
generate_square_subsequent_mask 函數(shù)
-
torch.ones(sz, sz):生成一個全是1的矩陣,形狀為 (sz, sz)。
-
torch.triu():將矩陣的下三角部分置為0,上三角部分保持為1。torch.triu(torch.ones(sz, sz)) 生成一個上三角矩陣。
-
transpose(0, 1):對矩陣進行轉置,使其符合注意力機制的輸入格式。
-
mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0)):將上三角矩陣中0的位置填充為負無窮,1的位置填充為0。
create_padding_mask 函數(shù)
用于生成一個填充mask,標記序列中的填充部分。具體來說,這個mask會告訴模型哪些位置是填充值(通常是0),模型在計算注意力時會忽略這些填充值,從而只關注有效的輸入。
在自然語言處理任務中,輸入序列通常具有不同的長度。為了使所有輸入序列具有相同的長度,通常會在較短的序列末尾添加填充值(通常為0)。但是,這些填充值在計算注意力時是不應該被考慮的,因為它們不包含實際信息。因此,需要一個mask來標記這些填充值的位置,使模型在計算注意力時忽略它們。
Input:
序列1: [5, 7, 2, 0, 0] 序列2: [1, 3, 0, 0, 0]
Output:
tensor([[False, False, False, True, True], [False, False, True, True, True]])