循環神經網絡(Recurrent Neural Network, RNN)是一類專門處理序列數據的神經網絡,如時間序列、自然語言、音頻等。與前饋神經網絡不同,RNN 引入了循環結構,能夠捕捉序列中的時序信息,使模型在不同時間步之間共享參數。這種結構賦予了 RNN 處理變長輸入、保留歷史信息的能力,成為序列建模的強大工具。
RNN 的基本原理與核心結構
傳統神經網絡在處理序列數據時,無法利用序列中的時序依賴關系。RNN 通過在網絡中引入循環連接,使得信息可以在不同時間步之間傳遞。
1. 簡單 RNN 的數學表達
在時間步t,RNN 的隱藏狀態\(h_t\)的計算如下:
\(h_t = \sigma(W_{hh}h_{t-1} + W_{xh}x_t + b)\)
其中,\(x_t\)是當前時間步的輸入,\(h_{t-1}\)是上一時間步的隱藏狀態,\(W_{hh}\)和\(W_{xh}\)是權重矩陣,b是偏置,\(\sigma\)是非線性激活函數(如 tanh 或 ReLU)。
2. RNN 的展開結構
雖然 RNN 在結構上包含循環,但在計算時通常將其展開為一個時間步序列。這種展開視圖更清晰地展示了 RNN 如何處理序列數據:
plaintext
x1 x2 x3 ... xT
| | | |
v v v v
h0 -> h1 -> h2 -> ... -> hT
| | | |
v v v v
y1 y2 y3 ... yT
其中,\(h_0\)通常初始化為零向量,\(y_t\)是時間步t的輸出(如果需要)。
3. RNN 的局限性
簡單 RNN 雖然能夠處理序列數據,但存在嚴重的梯度消失或梯度爆炸問題,導致難以學習長距離依賴關系。這限制了它在處理長序列時的性能。
長短期記憶網絡(LSTM)與門控循環單元(GRU)
為了解決簡單 RNN 的局限性,研究人員提出了更復雜的門控機制,主要包括 LSTM 和 GRU。
1. 長短期記憶網絡(LSTM)
LSTM 通過引入遺忘門、輸入門和輸出門,有效控制信息的流動:
\(\begin{aligned} f_t &= \sigma(W_f[h_{t-1}, x_t] + b_f) \\ i_t &= \sigma(W_i[h_{t-1}, x_t] + b_i) \\ o_t &= \sigma(W_o[h_{t-1}, x_t] + b_o) \\ \tilde{C}_t &= \tanh(W_C[h_{t-1}, x_t] + b_C) \\ C_t &= f_t \odot C_{t-1} + i_t \odot \tilde{C}_t \\ h_t &= o_t \odot \tanh(C_t) \end{aligned}\)
其中,\(f_t\)、\(i_t\)、\(o_t\)分別是遺忘門、輸入門和輸出門,\(C_t\)是細胞狀態,\(\odot\)表示逐元素乘法。
2. 門控循環單元(GRU)
GRU 是 LSTM 的簡化版本,合并了遺忘門和輸入門,并將細胞狀態和隱藏狀態合并:
\(\begin{aligned} z_t &= \sigma(W_z[h_{t-1}, x_t] + b_z) \\ r_t &= \sigma(W_r[h_{t-1}, x_t] + b_r) \\ \tilde{h}_t &= \tanh(W_h[r_t \odot h_{t-1}, x_t] + b_h) \\ h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \end{aligned}\)
其中,\(z_t\)是更新門,\(r_t\)是重置門。
RNN 的典型應用場景
RNN 在各種序列建模任務中取得了廣泛應用:
- 自然語言處理:機器翻譯、文本生成、情感分析、命名實體識別等。
- 語音識別:將語音信號轉換為文本。
- 時間序列預測:股票價格預測、天氣預測等。
- 視頻分析:動作識別、視頻描述生成。
- 音樂生成:自動作曲。
使用 PyTorch 實現 RNN 進行文本分類
下面我們使用 PyTorch 實現一個基于 LSTM 的文本分類模型,使用 IMDB 電影評論數據集進行情感分析。
python
運行
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.legacy import data, datasets
import random
import numpy as np# 設置隨機種子,保證結果可復現
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True# 定義字段
TEXT = data.Field(tokenize='spacy', tokenizer_language='en_core_web_sm', include_lengths=True)
LABEL = data.LabelField(dtype=torch.float)# 加載IMDB數據集
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)# 創建驗證集
train_data, valid_data = train_data.split(random_state=random.seed(SEED))# 構建詞匯表
MAX_VOCAB_SIZE = 25000
TEXT.build_vocab(train_data, max_size=MAX_VOCAB_SIZE, vectors="glove.6B.100d")
LABEL.build_vocab(train_data)# 創建迭代器
BATCH_SIZE = 64
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits((train_data, valid_data, test_data), batch_size=BATCH_SIZE,sort_within_batch=True,device=device)# 定義LSTM模型
class LSTMClassifier(nn.Module):def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout, pad_idx):super().__init__()# 嵌入層self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)# LSTM層self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=n_layers, bidirectional=bidirectional, dropout=dropout)# 全連接層self.fc = nn.Linear(hidden_dim * 2, output_dim)# Dropout層self.dropout = nn.Dropout(dropout)def forward(self, text, text_lengths):# text = [sent len, batch size]# 應用dropout到嵌入層embedded = self.dropout(self.embedding(text))# embedded = [sent len, batch size, emb dim]# 打包序列packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths.to('cpu'))# 通過LSTM層packed_output, (hidden, cell) = self.lstm(packed_embedded)# 展開序列output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)# output = [sent len, batch size, hid dim * num directions]# hidden = [num layers * num directions, batch size, hid dim]# cell = [num layers * num directions, batch size, hid dim]# 我們使用雙向LSTM的最終隱藏狀態hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1))# hidden = [batch size, hid dim * num directions]return self.fc(hidden)# 初始化模型
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]model = LSTMClassifier(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS, BIDIRECTIONAL, DROPOUT, PAD_IDX)# 加載預訓練的詞向量
pretrained_embeddings = TEXT.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)# 優化器和損失函數
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()model = model.to(device)
criterion = criterion.to(device)# 準確率計算函數
def binary_accuracy(preds, y):"""Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8"""# 四舍五入預測值rounded_preds = torch.round(torch.sigmoid(preds))correct = (rounded_preds == y).float() # 轉換為float計算準確率acc = correct.sum() / len(correct)return acc# 訓練函數
def train(model, iterator, optimizer, criterion):epoch_loss = 0epoch_acc = 0model.train()for batch in iterator:optimizer.zero_grad()text, text_lengths = batch.textpredictions = model(text, text_lengths).squeeze(1)loss = criterion(predictions, batch.label)acc = binary_accuracy(predictions, batch.label)loss.backward()optimizer.step()epoch_loss += loss.item()epoch_acc += acc.item()return epoch_loss / len(iterator), epoch_acc / len(iterator)# 評估函數
def evaluate(model, iterator, criterion):epoch_loss = 0epoch_acc = 0model.eval()with torch.no_grad():for batch in iterator:text, text_lengths = batch.textpredictions = model(text, text_lengths).squeeze(1)loss = criterion(predictions, batch.label)acc = binary_accuracy(predictions, batch.label)epoch_loss += loss.item()epoch_acc += acc.item()return epoch_loss / len(iterator), epoch_acc / len(iterator)# 訓練模型
N_EPOCHS = 5best_valid_loss = float('inf')for epoch in range(N_EPOCHS):train_loss, train_acc = train(model, train_iterator, optimizer, criterion)valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)if valid_loss < best_valid_loss:best_valid_loss = valid_losstorch.save(model.state_dict(), 'lstm-model.pt')print(f'Epoch: {epoch+1:02}')print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')# 測試模型
model.load_state_dict(torch.load('lstm-model.pt'))
test_loss, test_acc = evaluate(model, test_iterator, criterion)
print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
RNN 的挑戰與發展趨勢
盡管 RNN 在序列建模中取得了成功,但仍面臨一些挑戰:
- 長序列處理困難:即使是 LSTM 和 GRU,在處理極長序列時仍有困難。
- 并行計算能力有限:RNN 的時序依賴性導致難以高效并行化。
- 注意力機制的興起:注意力機制可以更靈活地捕獲序列中的長距離依賴,減少對完整歷史的依賴。
近年來,RNN 的發展趨勢包括:
- 注意力機制與 Transformer:注意力機制和 Transformer 架構在許多序列任務中取代了傳統 RNN,如 BERT、GPT 等模型。
- 混合架構:結合 RNN 和注意力機制的優點,如 Google 的 T5 模型。
- 少樣本學習與遷移學習:利用預訓練模型(如 XLNet、RoBERTa)進行微調,減少對大量標注數據的需求。
- 神經圖靈機與記憶網絡:增強 RNN 的記憶能力,使其能夠處理更復雜的推理任務。
循環神經網絡為序列數據處理提供了強大的工具,盡管面臨一些挑戰,但通過不斷的研究和創新,RNN 及其變體仍在眾多領域發揮著重要作用,并將繼續推動序列建模技術的發展。