目录
一、文本数据预处理:
1.文本预处理意义:
通过文本预处理建立字典可以巧妙地将回归问题转化为分类问题来处理。
2.文本预处理过程:
- 1.按行读入英文文本:
this is a good stroy ,
that is a pen . - 2.对文本标点进行处理:
this is a good story ’ '(这里表示标点转换为空格)
that is a pen ’ ’ - 3.对文本每行拆分token:
[“this”,“is”,“a”,“good”,“story”]
[“that”,“is”,“a”,“pen”] - 4.统计每个单词token出现的次数:
{<“this”,1>,<“is”,2>,<“a”,2>,<“good”,1>,<“story”,1>,<“that”,1>,<“pen”,1>} - 5.集合降序排序
{<“is”,2>,<“a”,2>,<“this”,1>,<“good”,1>,<“story”,1>,<“that”,1>,<“pen”,1>} - 6.根据token排序构建字典
{<“unk”,0>,<“is”,1>,<“a”,2>,<“this”,3>,<“good”,4>,<“story”,5>,<“that”,6>,<“pen”,7>} - 7.每个单词对应唯一的数字标识,这样原文本变成:
3 1 2 4 5
6 1 2 7
好处是数字类型相比于字符、字符串类型更方便模型处理 - 8.使用独热编码对每个单词进行编码,转成向量的形式:
3:[0,0,0,1,0,0,0,0],
1:[0,1,0,0,0,0,0,0]
… - 9.对每个句子进行整合,每个句子形成二维向量:
[
[0,0,0,1,0,0,0,0],
[0,1,0,0,0,0,0,0],
…
]
…
import collections
import re
from d2l import torch as d2l
# 读入一个文本数据(一本英文书)
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
'090b5e7e70c295757f55df93cb0a180b9691891a')
# 按行读入本文到lines,对标点进行处理
def read_time_machine():
with open(d2l.download('time_machine'), 'r') as f:
lines = f.readlines()
# 对于是A-Za-z的符号正常读入,其他符号转化成空格' '读入
return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
lines = read_time_machine()
# 将文本lines的每一行按空格拆分为单词或字符,每一个单词或字符就是一个token,这样操作后每行都是一个数组
def tokenize(lines, token='word'):
# 将文本lines的每一行拆分为单词
if token == 'word':
return [line.split() for line in lines]
# 将文本lines的每一行拆分为字符
elif token == 'char':
return [list(line) for line in lines]
else:
print('错误:未知令牌类型:' + token)
tokens = tokenize(lines)
# 统计文本tokens中每个单词出现的次数,返回(单词token,次数)键值对集合
def count_corpus(tokens):
if len(tokens) == 0 or isinstance(tokens[0], list):
tokens = [token for line in tokens for token in line]
return collections.Counter(tokens)
# 对文本构建字典,将每个单词映射成一个唯一标识的数字(字典序)方便训练
class Vocab:
def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
if tokens is None:
tokens = []
if reserved_tokens is None:
reserved_tokens = []
# 计算文本中每个单词出现次数键值对集合
counter = count_corpus(tokens)
# 根据出现次数对单词token进行降序排序
self.token_freqs = sorted(counter.items(), key=lambda x: x[1],
reverse=True)
# 对于unk和uniq_tokens一些特殊的token,unk的字典序记为0、uniq_tokens的字典序记为['<unk>'] + reserved_tokens
self.unk, uniq_tokens = 0, ['<unk>'] + reserved_tokens
# 对于其他单词token,将出现次数大于等于min_freq的单词token使用其次数排名作为字典序,出现次数小于min_freq的单词token直接丢弃
uniq_tokens += [
token for token, freq in self.token_freqs
if freq >= min_freq and token not in uniq_tokens]
# idx_to_token可以通过字典序查到对应的toekn单词,token_to_idx可以通过token单词查到其字典序
self.idx_to_token, self.token_to_idx = [], dict()
for token in uniq_tokens:
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token) - 1
def __len__(self):
return len(self.idx_to_token)
# 返回tokens数组中各个token的字典序
def __getitem__(self, tokens):
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk)
return [self.__getitem__(token) for token in tokens]
# 返回字典序数组中各个字典序的token单词
def to_tokens(self, indices):
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
return [self.idx_to_token[index] for index in indices]
vocab = Vocab(tokens)
# 将上面所有内容打包到该函数中
def load_corpus_time_machine(max_tokens=-1):
lines = read_time_machine()
tokens = tokenize(lines, 'char')
vocab = Vocab(tokens)
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens > 0:
corpus = corpus[:max_tokens]
# 返回字典和token列表
return corpus, vocab
corpus, vocab = load_corpus_time_machine()
二、传统神经网络如何优化成循环神经网络的:
1.传统神经网络:
2.循环神经网络:
循环神经网络为了关注序列的时序信息,在传统神经网络的网络结构不变的基础上,沿时间轴重复,来建立时序上的关联(并非神经元和隐藏层的增加,而是表示隐藏层在不同时刻的状态)。这种沿着时序反复迭代的网络结构,实现了对序列数据的学习,叫做RNN。
RNN的简洁表示如下:
3.踩坑注意!!!:
传统神经网络每次输入一个batch进行训练,但是上面的RNN图虽然是传统神经网络在时间轴上的堆叠,但是每个时间步的网络架构上并不是输入的一个batch,正确的是RNN每个时间步Xt的网络架构上输入的是一个batch中所有序列数据(所有样本)中的一个时间点或数据点token的集合。
三、循环神经网络定义:
对于一个句子的训练过程如下(一个batch中有batch_size个句子):
1.对于隐藏层的计算公式解释如下:
- ht表示t时刻隐藏层的输出,也叫做隐藏状态
- ht作为t时刻输出层的输入
- ht作为t+1时刻的隐藏状态,记录了某个batch的各个样本句子内前t+1个token单词数据点的序列信息
- 隐藏层中的参数有Whh、Whx、bh
- Whh表示隐藏层h关于上一层隐藏层输出的权重参数,即关于序列信息ht的权重参数
- Whx表示隐藏层h关于输入token数据点xt的权重参数
- bh表示隐藏层h的偏置值
- t时刻隐藏层的输入有ht-1、xt-1
- ht-1表示上一时刻隐藏层的输出,保存了样本序列已输入的t-1个数据点的时序信息
- xt-1表示该时刻隐藏层的输入
2.对于输出层的计算公式解释如下:
- ot表示t时刻输出层的输出
- 输出层中的参数有Woh、bo
- Woh表示输出层o关于输入ht的权重参数
- bo表示输出层o的偏置值
- t时刻隐藏层的输入有ht
- ht表示隐藏层的输出
3.细节:
- 因为模型结构固定的,只是时间上迭代,所以可学习参数W、b在各个时间维度上都是共享的。
- 训练阶段输入使用的是xt-1来预测,但是计算损失时使用的是xt来与预测值进行损失计算。
四、梯度裁剪:
五、token的概念:
一个token就是一个单词,一个句子中包含多个token,一个batch中包含多个句子。一个单词对应一个字典序,使用独热编码后就变为在各个字典序上的概率。
六、隐藏状态ht******:
对于一个batch(batch_size×num_steps×vocab_size)来说,ht维度为batch_size×vocab_size,拆解一下:一个句子内各个单词的序列关系的维度为1×vocab_size,batch_size个句子则构成ht(batch_size×vocab_size),ht作为t+1时刻的隐藏状态,记录了某个batch的各个样本句子内前t+1个token单词数据点的序列信息,表示每个句子(batch_size)内各个单词token(num_steps)的序列关系。训练时每个时间步(num_steps)会从batch_size个句子中各拿1个token单词进行训练,各个单词更新对应句子的隐藏状态一维向量,并且该一维向量记录了该句子中这个单词之前的序列信息。
隐藏状态ht仅作用于同一句子的各个单词token之间,同一batch中的不同句子之间没有时序关系,不同batch之间没有时序关系,每个batch训练开始时都会初始化h0,不会使用上一个batch的ht。
上图为一个句子的训练过程如下(一个batch中有batch_size句子),此时ht维度为1×vocab_size(batch_size=1),在ht时输入token为’,',此时一维向量ht记录了该单词之前’你好’的序列信息。
七、训练过程举例**********:
以下文预测问题为例,一次epoch训练过程如下。
1.对整个文本进行数据预处理,获得数据字典,这里假设字典中有vocab_size条字典序,这样就转换成了一个vocab_size分类的序列问题。
2.将每个单词token值使用独热编码转换成1×vocab_size的一维向量,作为特征,表示各分类上的概率。
3.每轮epoch输入格式为batch_num×batch_size×num_steps×vocab_size,其中batch_num表示该轮压迫训练多少个batch,batch_size表示每个batch中有多少个句子序列,每个句子有num_steps个单词token,即该batch要训练多少个时间步,即循环time_step次传统神经网络,每个单词为一个一维向量,表示在字典序上的概率。每次训练一个batch,每个时间步t使用该batch中所有batch_size个序列的第t个token集合Xt进行训练(num_steps=t的token),batch尺寸为batch_size×num_steps×vocab_size,Xt尺寸为batch_size×vocab_size。
4.隐藏层参数Whh维度为num_hiddens×num_hiddens,表示隐藏层关于序列信息(隐藏状态)的权重矩阵;Whx维度为vocab_size×num_hiddens,表示隐藏层关于输入特征的权重矩阵;参数bh维度为1×num_hiddens。
5.对于第一个batch,训练过程如下:
5.1.初始化0时刻序列信息(隐藏层输出,隐藏状态)h0,尺寸为(batch_size,神经元个数num_hiddens)。
5.2.t1时间步num_steps=1,取该batch所有序列样本的第一个token组成x0,尺寸batch_size×vocab_size,每个vocab一维向量并行放入神经网络学习,h1=sigmoid(Whh×h0+Whx×x0+bh),每个token输出维度1×num_hiddens,隐藏层输出维度batch_size×num_hiddens,h1作为t1时间步的输出层输入、t2时间步的隐藏层输入序列信息(隐藏状态)。
5.3.此时两个操作并行执行:t1时间步的输出层计算、t2时间步的隐藏层计算。
5.3.1首先h1作为t1时间步的输出层输入,输出层有vocab_size个神经元,会执行多分类预测,可学习参数为Woh(num_hiddens×vocab_size)和bo(1×vocab_size),每个token输出维度1×vocab_size,输出层输出维度batch_size×vocab_size,表示各个token在各个分类上的预测。
5.3.2其次,t2时间步num_steps=2,取batch中num_steps=2的token集合为x1,维度为batch_size×vocab_size,并行将每个token一维向量放入神经网络学习,隐藏层输出h2=sigmoid(Whh×h1+Whx×x1+bh),每个token输出维度1×num_hiddens,隐藏层输出维度batch_size×num_hiddens,h2作为t2时间步的输出层输入、t3时间步的隐藏层输入序列信息。
5.4.如此反复每个时间步取一个数据点token集合进行训练,并更新隐藏层输出ht作为下一个时间步的输入,直到完成所有num_steps个时间步的训练任务,整个batch就训练完成了。
5.5.对于每个时间步上的预测batch_size×vocab_size,num_steps个时间步上总的预测为(num_steps×batch_size,vocab_size),这是该batch的训练总输出。
5.6.使用损失函数计算batch中各个句子中每个token的概率损失,并取均值。
5.7.反向传播算法计算各个参数Whh、Whx、bh、Woh、bo关于损失函数的梯度。
5.8.梯度裁剪修改梯度。
5.9.梯度下降算法修改参数值。
6.该batch训练完成。进行下一个batch训练,初始化隐藏状态h0…。
八、预测过程举例**********:
背景定义同训练过程,模型的预测过程如下。
1.输入prefix长度的前缀,来预测接下来num_preds个token。
2.首先还是将prefix转换成字典序并进行独热编码,尺寸为1×prefix×vocab_size,其中prefix=num_steps。
3.加载模型,初始化时序信息h0。
4.batch_size为1,在每个时间步上对句子长度每个token一维向量依次作为模型一个时间步的输入,输入维度1×vocab_size,总共计算prefix个时间步,循环计算prefix个时间步后的时序信息hp,hp尺寸为1×num_hiddens(batch_size=1)。
5.将prefix最后一个token和hp作为模型输入,来预测num_preds个token的第一个token,输出预测结果pred1和时序信息hp1,然后将pred1和hp1作为输入预测pred2和hp2(即使用预测值来预测下一个预测值),直到预测num_preds个预测值。(等价于batch=1,num_steps=num_preds的训练过程)
6.将预测值使用字典转为字符串输出。
九、图书下文预测代码实现:
1.底层源码:
import math
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
# 数据预处理,返回字典vocab和dataloader
batch_size, num_steps = 32, 35# 每个batch有batch_size个序列,每个序列中有num_steps个时间步的数据点token
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# voacab长度为28,batch_size尺寸为32×35
# RNN单层不循环网络模型,初始化可学习参数
def get_params(vocab_size, num_hiddens, device):
# 每个token的输入维度为字典大小[0,0,....,1,0,.....,0],做分类问题,输出维度也为字典大小[0.1,0.2,0,.....,0.3,....,0]
num_inputs = num_outputs = vocab_size
# 可学习参数初始化
def normal(shape):
return torch.randn(size=shape, device=device) * 0.01
# 因为模型结构固定的,只是时间步上迭代,所以参数在各个时间维度上都是共享的(除了ht每个batch都要赋初值)
W_xh = normal((num_inputs, num_hiddens))# 尺寸为样本特征数×隐藏层神经元个数(全连接层)
W_hh = normal((num_hiddens, num_hiddens))# 隐藏层神经元个数×隐藏层神经元个数
b_h = torch.zeros(num_hiddens, device=device)# 尺寸为隐藏层神经元个数 != 样本特征数
W_hq = normal((num_hiddens, num_outputs))# num_outputs=vocab_size
b_q = torch.zeros(num_outputs, device=device)
params = [W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
# 每个batch开始时初始化0时刻隐藏层输出h0
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),)# 尺寸为隐藏层输出尺寸:样本数batch_size×每个样本句子中各个单词之间的序列关系num_hiddens
# 向前传播,使用1个batch计算n个时间步to~tn上的预测
def rnn(inputs, state, params):# inputs为三维张量(批量大小batch_size×时间步数num_steps×字典长度vocab_size),state为上一个时间步的隐藏层输出h0(样本句子数batch_size×每个样本内单词token的序列关系num_hiddens)
# 隐藏层和输出层的可学习参数
W_xh, W_hh, b_h, W_hq, b_q = params
# 上一时间步(t-1)的隐藏层输出,这个全局变量定义的很妙,下面的每次循环都会改变它的值
H, = state
outputs = []
# 每次取出一个时间步的token计算隐藏层输出和预测值,循环num_steps次
for X in inputs:
# 该时间步上隐藏层输出,注意这里的H始终在上一时间步被赋值然后应用于该时间步上隐藏层的计算
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
# 该时间步上输出层输出=隐藏层输出H×输出层权重参数W_hq+偏置值
Y = torch.mm(H, W_hq) + b_q
# num_steps个时间步上的输出叠加成总输出
outputs.append(Y)
# 返回num_steps个时间步上的预测(应该是num_steps×batch_size×vocab_size,但这里是在第0个维度拼接,所以尺寸为二维:(num_steps×batch_size,vocab_size),最后一个时间步的隐藏层输出H(样本数batch_size×每个样本的序列信息)
return torch.cat(outputs, dim=0), (H,)
# 训练函数,包装类包含上面的函数
class RNNModelScratch:
def __init__(self, vocab_size, num_hiddens, device, get_params,
init_state, forward_fn):
# 初始化字典长度,神经元个数
self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
# 初始化可学习参数
self.params = get_params(vocab_size, num_hiddens, device)
# 初始化隐藏层输出h0,初始化向前传播过程
self.init_state, self.forward_fn = init_state, forward_fn
def __call__(self, X, state):
# 将数据集每个样本句子中的每个单词token使用都热编码转换成字典上的概率一维向量,这样每个句子都变为2维
X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
# 进行模型向前传播
return self.forward_fn(X, state, self.params)
# 获取当前时间步隐藏层输出
def begin_state(self, batch_size, device):
return self.init_state(batch_size, self.num_hiddens, device)
# 预测函数,给出一个前缀prefix,使用net模型预测num_preds个值,并将预测值转换成字典vocab对应的字符串
def predict_ch8(prefix, num_preds, net, vocab, device):
# 初始化隐藏层输出h0
state = net.begin_state(batch_size=1, device=device)
# 将前缀字符串列表最前面一个字符串拿出来转换成字典序赋值给outputs
outputs = [vocab[prefix[0]]]
# 将outputs中最后一个字符串拿出来
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape(
(1, 1))
# 遍历前缀字符串列表,得到最终隐藏层输出
for y in prefix[1:]:
# 每次拿出最前面的一个字符串转成字典序后放入模型计算隐藏层输出state
_, state = net(get_input(), state)
# 依次将prefix中的字符串加入到outputs中,实现每次get_input拿一个字符串
outputs.append(vocab[y])
# 第一次循环使用prefix最后一个字符串预测第一个字符串Y,第二次循环使用第一个预测值Y预测第二个预测值字符串
for _ in range(num_preds):
# 每次输入一个字符串预测下一个字符串Y,并更新隐藏层输出
y, state = net(get_input(), state)
# 每次将预测值Y放入outputs作为下一次循环的输入
outputs.append(int(y.argmax(dim=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
# 梯度裁剪防止梯度爆炸
def grad_clipping(net, theta):
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad**2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
# 一次epoch训练过程
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2)
# 对一个batch
for X, Y in train_iter:
# 初始化隐藏层输出
if state is None or use_random_iter:
state = net.begin_state(batch_size=X.shape[0], device=device)
else:
if isinstance(net, nn.Module) and not isinstance(state, tuple):
state.detach_()
else:
for s in state:
s.detach_()
y = Y.T.reshape(-1)
X, y = X.to(device), y.to(device)
# 向前转播计算epoch上的预测值
y_hat, state = net(X, state)
# 计算损失
l = loss(y_hat, y.long()).mean()
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
# 计算梯度
l.backward()
# 梯度裁剪
grad_clipping(net, 1)
# 更新参数
updater.step()
else:
l.backward()
grad_clipping(net, 1)
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
# 返回困惑度
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
# 完整训练过程
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
# 多分类损失函数
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
legend=['train'], xlim=[10, num_epochs])
if isinstance(net, nn.Module):
updater = torch.optim.SGD(net.parameters(), lr)
else:
updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
# 封装预测函数,往后预测50个token
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
# 训练epoch轮
for epoch in range(num_epochs):
# 每个epoch都调用train_epoch_ch8
ppl, speed = train_epoch_ch8(net, train_iter, loss, updater, device,
use_random_iter)
if (epoch + 1) % 10 == 0:
print(predict('time traveller'))
animator.add(epoch + 1, [ppl])
print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}')
print(predict('time traveller'))
print(predict('traveller'))
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
init_rnn_state, rnn)
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())
2.Pytorch版代码:
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
# 数据预处理,返回字典vocab和dataloader
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 定义模型单层不循环架构
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
# 初始化隐藏层输出h0
state = torch.zeros((1, batch_size, num_hiddens))
# RNN架构
class RNNModel(nn.Module):
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
def forward(self, inputs, state):
X = F.one_hot(inputs.T.long(), self.vocab_size)
X = X.to(torch.float32)
Y, state = self.rnn(X, state)
output = self.linear(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, device, batch_size=1):
if not isinstance(self.rnn, nn.LSTM):
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device)
else:
return (torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device),
torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device))
# 使用高级API训练模型
device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)
# 使用高级API预测
d2l.predict_ch8('time traveller', 10, net, vocab, device)
十、深度循环神经网络:
1.定义:
单隐藏层RNN、GRU、LSTM都是只有一层隐藏层(隐藏层并行不算)的RNN。
深度RNN即具有多个隐藏层堆叠,这样更容易获得更多的非线性特征。
- 单层RNN结构:
- 深度RNN结构:
2.代码:
这里两个隐藏层神经元个数都是256,其实可以分别设置每个隐藏层神经元个数
import torch
from torch import nn
from d2l import torch as d2l
# 数据预处理,返回字典vocab和dataloader
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 通过num_layers的值来设定隐藏层数
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
# 训练
num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
十一、双向循环神经网络:
1.定义:
这里其实是两个隐藏层的双向RNN,第一个隐藏层是对序列从第一个词开始进行正向计算,第二个隐藏层对序列从最后一个词开始进行反向计算。并且两个隐藏层是独立的,第二个隐藏层不需要第一个隐藏层的输出作为输入。两个层是分开算输出的,正向层和以前一样,反向层第一输入的X值是一句话的最后一个词,然后到第一个词。每个时间步的输出相加时将正向的输出和反向的输出转一下相加就得到了。
注意:双向RNN通常用来对序列抽取特征、填空,而不能预测未来。