循环神经网络
更新隐藏状态:
h
t
=
ϕ
(
W
h
h
h
t
−
1
+
W
h
x
x
t
−
1
+
b
h
)
h_t = \phi(W_{hh}h_{t-1}+W_{hx}x_{t-1}+b_h)
ht=ϕ(Whhht−1+Whxxt−1+bh)
输出:
o
t
=
ϕ
(
W
h
o
h
t
+
b
o
)
o_t = \phi(W_{ho}h_t+b_o)
ot=ϕ(Whoht+bo)
容易注意到,去掉隐藏层后(
W
h
h
h
t
−
1
W_{hh}h_{t-1}
Whhht−1这一项),和MLP完全一样
困惑度(perplexity)
需要量化预测结果的好坏。
衡量一个语言模型的好坏可以用平均交叉熵(分类问题,有字典大小个类别的分类):
π
=
1
n
∑
i
=
1
n
−
l
o
g
p
(
x
t
∣
x
t
−
1
,
⋯
)
\pi = \frac 1 n\sum^n_{i=1} -log\ p(x_t|x_{t-1},\cdots)
π=n1i=1∑n−log p(xt∣xt−1,⋯)
p是语言模型的预测概率,
x
t
x_t
xt是真实词。
由于历史原因,NLP使用困惑度 e x p ( π ) exp(\pi) exp(π) ,1表示完美,无穷大是最差情况
梯度裁剪
迭代中计算T个时间步上的梯度,在反向传播过程中产生长度为 O ( T ) O(T) O(T)的矩阵乘法链,导致数值不稳定。梯度裁剪能有效预防梯度爆炸:
如果梯度长度超过
θ
\theta
θ,那么拖影回长度
θ
\theta
θ:
g
←
m
i
n
(
1
,
θ
∣
∣
g
∣
∣
)
g
g\leftarrow min(1,\frac{\theta}{||g||})g
g←min(1,∣∣g∣∣θ)g
如果
∣
∣
g
∣
∣
>
θ
||g|| > \theta
∣∣g∣∣>θ,则会取到
θ
∣
∣
g
∣
∣
\frac{\theta}{||g||}
∣∣g∣∣θ
更多的RNN应用
RNN的实现
1.独立热编码
在训练数据中,每个词元都表示为一个数字索引,将这些索引直接输入神经网络可能会使学习变得困难,我们通常将每个词元表示为更具表现力的特征向量,最简单的标识为独热编码。
每次采样的小批量数据形状是二维张量: (批量大小,时间步数)。 one_hot
函数将这样一个小批量数据转换成三维张量, 张量的最后一个维度等于词表大小(len(vocab)
)。 我们经常转换输入的维度,以便获得形状为 (时间步数,批量大小,词表大小)的输出。 这将使我们能够更方便地通过最外层的维度, 一步一步地更新小批量数据的隐状态。
'''独立热编码,vocab有28个,26个字母+空格+unk'''
print(F.one_hot(torch.tensor([0, 2]), len(vocab))) # 下标0和2为1
X = torch.arange(10).reshape((2, 5))
print(F.one_hot(X.T, 28).shape)
2.循环神经网络模型
'''初始化参数'''
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size #独热编码后是one hot,则输入输出的维度都是词表的大小
def normal(shape):
return torch.randn(size=shape, device=device) * 0.01
# 隐藏层参数
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))
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
# 给定一个初始的隐藏状态
# 使用该函数在初始化时返回隐状态,返回是一个全0张量,形状为(批量大小,隐藏单元数)
def init_rnn_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),)
# 做计算
# state 是隐藏状态,params是学习的参数
def rnn(inputs, state, params):
# inputs的形状:(时间步数量,批量大小,词表大小)
W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:#按时刻进行遍历,所以one_hot时做个转置,将时间维度放在第一维度,方便遍历
# mm做矩阵乘法,H是前一个隐藏状态,使用tanh做激活函数
H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
Y = torch.mm(H, W_hq) + b_q
outputs.append(Y)
# 输出还要加上当前的隐藏状态,因为可能还要使用
# 在维度0拼接,拼接成一个二维的矩阵,列数没别(vocb_size),行数变为批量大小乘时间长度
return torch.cat(outputs, dim=0), (H,)
'''包装成一个类'''
class RNNModelScratch: #@save
"""从零开始实现的循环神经网络模型"""
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)
self.init_state, self.forward_fn = init_state, forward_fn
def __call__(self, X, state):
# one_hot出来是个整型,变换为浮点,才能做前向传播
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)
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
# X是2*5的矩阵,即10个词,那么Y就是(10,28)的矩阵,对每个词都有28分类
# new_state 是长为1的元组
Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape
3.预测
def predict_ch8(prefix, num_preds, net, vocab, device): #@save
"""在prefix后面生成新字符,num_preds就是预测多少个词"""
state = net.begin_state(batch_size=1, device=device)
outputs = [vocab[prefix[0]]]
# input函数就是将最近预测的一个值作为输入,批量大小为1,时间步长为1
get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
for y in prefix[1:]: # 预热期,不需要进行预测,只是用于初始化状态
_, state = net(get_input(), state)
outputs.append(vocab[y])#直接用真实值
for _ in range(num_preds): # 预测num_preds步
y, state = net(get_input(), state)
#argmax就是softmax,找到最大可能的类别,reshape成一个标量,即下标
outputs.append(int(y.argmax(dim=1).reshape(1)))
return ''.join([vocab.idx_to_token[i] for i in outputs])
predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())
4.梯度裁剪
前文中提过,如果在迭代中计算T个时间步上的梯度,将会在反向传播中产生长度为 O ( T ) O(T) O(T)的矩阵乘法链,大道至梯度爆炸或者梯度消失,所以需要用额外的方式来支持稳定训练
一般来说,假定在向量形式的 x x x中,或者在小批量数据的负梯度 g g g方向上,用 η \eta η作为学习率是,在一次迭代中,我们将 x x x更新为 x − η g x-\eta g x−ηg,如果我们进一步假设目标函数 f f f表现良好,即函数 f f f在常数 L L L下是利普希茨连续的。
利普希茨连续(Lipschitz continuous),对于任意 x , y x,y x,y有:
∣ f ( x ) − f ( y ) ∣ ≤ L ∣ ∣ x − y ∣ ∣ |f(x)-f(y)|\le L||x-y|| ∣f(x)−f(y)∣≤L∣∣x−y∣∣
在这种情况下,可以假设:如果我们通过
η
g
\eta g
ηg更新参数向量,则:
∣
f
(
x
)
−
f
(
x
−
η
g
)
∣
≤
L
η
∣
∣
g
∣
∣
|f(x)-f(x-\eta g)|\le L\eta||g||
∣f(x)−f(x−ηg)∣≤Lη∣∣g∣∣
这意味着我们不悔观察到超过$ L\eta||g||$的变化,这有好有坏,它限制了取得进展的素的,也限制了变坏的程度,尤其是朝着错误的方向前进时。
有时梯度可能很大,从而优化算法无法收敛,我们可以通过降低
η
\eta
η的学习率来解决这个问题,但我们可能很少得到大的梯度,所以一个流行的替代方案是通过将梯度
g
g
g投影回给定半径(
例如
θ
例如\theta
例如θ)的球来裁剪梯度
g
g
g,如下式:
g
←
m
i
n
(
1
,
θ
∣
∣
g
∣
∣
)
g
g\leftarrow min(1,\frac{\theta}{||g||})g
g←min(1,∣∣g∣∣θ)g
通过这样做,梯度范数将永远不会超过
θ
\theta
θ,并且更新后的梯度完全与原始方向对齐。它还有一个好处,即限制任何给定的小批量数据(以及其中任何给定的样本)对参数向量的影响,这赋予了模型一定程度的稳定性
def grad_clipping(net, theta): #@save
"""裁剪梯度,params是全局的"""
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
5.训练
#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
"""训练网络一个迭代周期(定义见第8章)"""
state, timer = None, d2l.Timer()
metric = d2l.Accumulator(2) # 训练损失之和,词元数量
for X, Y in train_iter:
if state is None or use_random_iter:
# 在第一次迭代或使用随机抽样时初始化state
state = net.begin_state(batch_size=X.shape[0], device=device)
else:
if isinstance(net, nn.Module) and not isinstance(state, tuple):
# state对于nn.GRU是个张量,一个张柳,直接分离出来
state.detach_() # 连续的,将上一个批量的状态传递一下
else:
# state对于nn.LSTM或对于我们从零开始实现的模型是个张量
# state是个元组,多个张量,分别分离出来,不做梯度运算
for s in state:
s.detach_()
y = Y.T.reshape(-1) # 转置把时间步放到第一维度
X, y = X.to(device), y.to(device)
y_hat, state = net(X, state)
l = loss(y_hat, y.long()).mean()
# loss就是把他拉成一维向量,那么第一维度是时间步数就很关键
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
l.backward()
grad_clipping(net, 1) #梯度裁剪
updater.step()
else:
l.backward()
grad_clipping(net, 1)
# 因为已经调用了mean函数
updater(batch_size=1)
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
use_random_iter=False):
"""训练模型(定义见第8章)"""
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)
predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
# 训练和预测
for epoch in range(num_epochs):
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_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
init_rnn_state, rnn)
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
use_random_iter=True)
词还是对的,但没什么联系。。。(困惑度和是随机取样的方式)
6.简洁实现
库要快一些,因为从零实现做了很多次小矩阵乘法,而pytorch的rnn库里将小矩阵连接起来,形成一个大矩阵,只做一次大矩阵乘法,要快3倍左右。
'''简洁实现'''
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)
state = torch.zeros((1, batch_size, num_hiddens))
state.shape
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape # 时间,批量大小,隐藏层数量
#(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))
#pytorch的rnn类只有隐藏层,没有输出层
#@save
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
# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
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)
# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
# 它的输出形状是(时间步数*批量大小,词表大小)。
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):
# nn.GRU以张量作为隐状态
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device)
else:
# nn.LSTM以元组作为隐状态
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))