B 站视频讲解
本文介绍一下如何使用 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())))}
VOCAB_SIZE = len(word2idx) # classification problem
max_len = len(sentence.split())
HIDDEN_SIZE = 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, VOCAB_SIZE]
batch_size=16
input_batch, target_batch = make_data(sentence)
dataset = Data.TensorDataset(input_batch, target_batch)
#由于input_batch不能整除batch_size,所以要drop_last=True
loader = Data.DataLoader(dataset,batch_size,shuffle=True,drop_last=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,vocab_size,hidden_size):
super(BiLSTM, self).__init__()
self.lstm = nn.LSTM(input_size=vocab_size, hidden_size=hidden_size, bidirectional=True)
# fc
self.fc = nn.Linear(hidden_size * 2, vocab_size)
def forward(self, X,h_0,c_0):
# X: [batch_size, seq_len, vocab_size]
batch_size = X.shape[0]
input = X.transpose(0, 1) # input : [seq_len, batch_size, vocab_size]
outputs, (_, _) = self.lstm(input, (h_0, c_0))
outputs = outputs[-1] # [batch_size, hidden_size * 2]
prep = self.fc(outputs) # model : [batch_size, vocab_size]
return prep
model = BiLSTM(VOCAB_SIZE,HIDDEN_SIZE)
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:
hidden_state = torch.randn(1*2, batch_size, HIDDEN_SIZE) # [num_layers(=1) * num_directions(=2), batch_size,hidden_size]
cell_state = torch.randn(1*2, batch_size, HIDDEN_SIZE) # [num_layers(=1) * num_directions(=2), batch_size, hidden_size]
pred = model(x,hidden_state,cell_state)
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()
测试
hidden_state = torch.randn(1*2, batch_size, HIDDEN_SIZE) # [num_layers(=1) * num_directions(=2), batch_size, n_hidden]
cell_state = torch.randn(1*2, batch_size, HIDDEN_SIZE)
predict = model(input_batch[:16],hidden_state,cell_state).data.max(1, keepdim=True)[1]
print(sentence)
print([idx2word[n.item()] for n in predict.squeeze()])