本文介绍一下如何使用 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()])
写的真的挺好,受益非亲。
但是有个问题,想请教一下:
loader = Data.DataLoader(dataset, 16, True)
这里的第二个参数,16 是怎么来的?
以及,这个参数具体指什么?
16 是 batchsize,根据自己的显存大小设置
你好,亲爱的博主,想请问一下,对于 BilLSTM 那幅图的理解,我有关于以下的问题:Input layer 那里的 x0,x1,x2,...,xn 代表的是对输入文本进行分词后,每个 token 的的词向量吗?那么 forward layer (或者 backward layer) 的单个 LSTM 单元就有 n 个咯?那么 y1,y2,...,yn 代表的是什么呢?
x0,x1,x2,...,xn 代表的是每个 token 的词向量是 n 个
y1,y2,...,yn 代表的是模型的输出,输出也是个向量,只不过维度和 x0,...,xn 不一定一样
作者您好,请问为什么再处理数据的时候要用 0 来填充长度?
input = input + [0] * (max_len - len(input))
0 在字典中也表示一个单词,为什么不用 - 1 或者其他没有实际意义的索引来填充呢?
有道理,确实不太应该用 0 来填充
谢谢!我见过的最快回复,哈哈,还有一个问题。为什么在取数据的时候,只循环 max_len-1 次呢?这样的话最后一个单词 more 并没有作为 input 吗?
假如现在有一个句子:“hello world”,你觉得需要循环多少次?答案是 1 次,即输入 “hello [pad]” 让他输出 "world“
如果按照您的意思,输入 "hello world",那么请问它需要预测什么东西呢?
got it!如果是有起始符的话,这地方是需要的吧。 输入 more 来预测 [end]
您说的这种情况确实是要(seq2seq 问题),但是在我这篇文章中不涉及到这些特殊 token
好的!多谢您的解答!
你好博主,看了多篇的文章,一直很好奇,这个 x 的输入 是怎么得来的,没有看到定义 x 的地方,他是默认的神经网络的输入值吗?
def forward(self, X):
# X: [batch_size, max_len, n_class] batch_size = X.shape[0] input = X.transpose(0, 1)pred = model (x) 里面的 x 啊,这你没看到吗 x 就是 for x, y in loader:
我感觉反向 lstm 没有用处,一开始都设置为 0,这样反向的话也没有提供什么有用信息,感觉还是单向的 lstm 在进行预测,不知道对不对?
我想的是,如果预测 “i like your friend” 这句话里面的 “your”,input 是不是应该是 “i like x friend"(所对应的索引
博主你好,感谢分享这篇文章,我想请教一些问题。
1. 这个网络可以预测的所有单次是否就是 sentence 中的所有单词?
2. 这个网络的学习集是否可以理解为,sentence 中不同长度的顺序排列(输入)以及其后一个单词(输出)?
3. 学习集和预测集是相同的?
博主 您好 “outputs, (_, _) = self.lstm (input, (hidden_state, cell_state))” 这个隐藏状态是一个列表吗? 如果但我想使用 Bilstm 作为 seq2seq 中的编码器的,那么它的最后一层隐藏状态该怎么取?
遇到问题自己多打印,不会的查官方 api
up 您好,小白想问一个问题,请问这里 input_batch.append (np.eye (n_class)[input]),因为您做的是预测单词,所以这里是分类问题,我想用 bilstm 做时间序列的预测,那这里就不是一个分类问题(预测出来的可能取的数值是无穷多),请问我应该怎么处理呢
如果预测 “i like your friend” 这句话里面的 “your”,input 是不是应该是 “i like x friend"(所对应的索引
我怎么感觉这不是 BiLSTM,假如有个句子是 “I like your friends very much” 双向 LSTM 难道不是正向 'I like your' 反向 "much very" 用这两方面的信息去推理 friends 吗
写的太好了,赞一个