本文介紹一下如何使用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)

這裏面的循環還是有點複雜的,尤其是 inputinput_batch 裏面存的東西,很難理解。所以下面我會詳細解釋

首先開始循環, input 的第一個賦值語句會將第一個詞 Github 對應的索引存起來。 input 的第二個賦值語句會將剩下的 max_len - len(input) 都用0去填充

第二次循環, input 的第一個賦值語句會將前兩個詞 GithubActions 對應的索引存起來。 input 的第二個賦值語句會將剩下的 max_len - len(input) 都用0去填充

每次循環, inputtarget 中所存的 索引轉換成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()])
相關文章