BiLSTM的PyTorch應用
本文介紹一下如何使用BiLSTM(基於PyTorch)解決一個實際問題,實現 給定一個長句子預測下一個單詞
如果不瞭解LSTM的同學請先看我的這兩篇文章LSTM、PyTorch中的LSTM。下面直接開始代碼講解
導庫
''' code by Tae Hwan Jung(Jeff Jung) @graykode, modify by wmathor ''' import torch import numpy as np import torch.nn as nn import torch.optim as optim import torch.utils.data as Data dtype = torch.FloatTensor
準備數據
sentence = ( 'GitHub Actions makes it easy to automate all your software workflows ' 'from continuous integration and delivery to issue triage and more' ) word2idx = {w: i for i, w in enumerate(list(set(sentence.split())))} idx2word = {i: w for i, w in enumerate(list(set(sentence.split())))} n_class = len(word2idx) # classification problem max_len = len(sentence.split()) n_hidden = 5
我水平不佳,一開始看到這個 sentence
不懂這種寫法是什麼意思,如果你調用 type(sentence)
以及打印 sentence
就會知道,這其實就是個字符串,就是將上下兩行字符串連接在一起的一個大字符串
數據預處理,構建dataset,定義dataloader
def make_data(sentence): input_batch = [] target_batch = [] words = sentence.split() for i in range(max_len - 1): input = [word2idx[n] for n in words[:(i + 1)]] input = input + [0] * (max_len - len(input)) target = word2idx[words[i + 1]] input_batch.append(np.eye(n_class)[input]) target_batch.append(target) return torch.Tensor(input_batch), torch.LongTensor(target_batch) # input_batch: [max_len - 1, max_len, n_class] input_batch, target_batch = make_data(sentence) dataset = Data.TensorDataset(input_batch, target_batch) loader = Data.DataLoader(dataset, 16, True)
這裏面的循環還是有點複雜的,尤其是 input
和 input_batch
裏面存的東西,很難理解。所以下面我會詳細解釋
首先開始循環, input
的第一個賦值語句會將第一個詞 Github
對應的索引存起來。 input
的第二個賦值語句會將剩下的 max_len - len(input)
都用0去填充
第二次循環, input
的第一個賦值語句會將前兩個詞 Github
和 Actions
對應的索引存起來。 input
的第二個賦值語句會將剩下的 max_len - len(input)
都用0去填充
每次循環, input
和 target
中所存的 索引轉換成word
如下圖所示,因爲我懶得去查看每個詞對應的索引是什麼,所以乾脆直接寫出存在其中的詞
從上圖可以看出, input
的長度永遠保持 max_len(=21)
,並且循環了 max_len-1
次,所以最終 input_batch
的維度是 [max_len - 1, max_len, n_class]
定義網絡架構
class BiLSTM(nn.Module): def __init__(self): super(BiLSTM, self).__init__() self.lstm = nn.LSTM(input_size=n_class, hidden_size=n_hidden, bidirectional=True) # fc self.fc = nn.Linear(n_hidden * 2, n_class) def forward(self, X): # X: [batch_size, max_len, n_class] batch_size = X.shape[0] input = X.transpose(0, 1) # input : [max_len, batch_size, n_class] hidden_state = torch.randn(1*2, batch_size, n_hidden) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden] cell_state = torch.randn(1*2, batch_size, n_hidden) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden] outputs, (_, _) = self.lstm(input, (hidden_state, cell_state)) outputs = outputs[-1] # [batch_size, n_hidden * 2] model = self.fc(outputs) # model : [batch_size, n_class] return model model = BiLSTM() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001)
Bi-LSTM的網絡結構圖如下所示,其中Backward Layer意思不是"反向傳播",而是將"句子反向輸入"。具體流程就是,現有有由四個詞構成的一句話"i like your friends"。常規單向LSTM的做法就是直接輸入"i like your",然後預測出"friends",而雙向LSTM會同時輸入"i like your"和"your like i",然後將Forward Layer和Backward Layer的output進行concat(這樣做可以理解爲同時"汲取"正向和反向的信息),最後預測出"friends"
而正因爲多了一個反向的輸入,所以整個網絡結構中很多隱藏層的輸入和輸出的某些維度會變爲原來的兩倍,具體如下圖所示。對於雙向LSTM來說, num_directions = 2
訓練&測試
# Training for epoch in range(10000): for x, y in loader: pred = model(x) loss = criterion(pred, y) if (epoch + 1) % 1000 == 0: print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss)) optimizer.zero_grad() loss.backward() optimizer.step() # Pred predict = model(input_batch).data.max(1, keepdim=True)[1] print(sentence) print([idx2word[n.item()] for n in predict.squeeze()])