手敲一遍,抱抱佛脚~

语言模型

一个语言序列的生成概率如下:

例如:

在上式中,可能会有一些疑惑,那就是、指的到底是什么?

  • 前者可以计算为在训练集中的词频(出现次数)与训练集总次数的比值
  • 后者可以计算为三词相邻的频率与相邻的频率的比值

拉普拉斯平滑处理

基于条件概率计算时,由于单词之间的不常见组合在训练集中出现次数可能非常稀少,导致计算出某个组合的概率直接为0或非常接近于零,那么这显然是不切于实际的。为了防止这类问题,我们可以引入拉普拉斯平滑处理
设n为训练集中单词总数,m为该非寻常的单词出现总数:

其中都是超参数,取0时不进行平滑处理。当逼近正无穷时近似等于,通过这种方法,123式对于平滑的处理程度会逐渐变弱,这也符合常理,在越长的context中出现的机率显然越来越小。通过这种方法,可以有效避免某个组合的概率直接为0或非常接近于零的问题

n-grams

但是这个概率显然并不好算,不妨利用马尔科夫链的假设,即当前这个词仅仅跟前面几个有限的词相关,因此也就不必追溯到最开始的那个词,这样便可以大幅缩减上述算式的长度。相当于加上了一个滑动窗口,每次只查看到该词的第前n个词
基于阶马尔科夫链,可以将语言模型改写为:

RNN

此前讲的神经网络都是不含隐藏状态的

不含隐藏状态的神经网络

就是MLP,没有什么好说的

含隐藏状态的循环神经网络

与多层感知机不同的是,这里我们保存上一时间步的隐藏变量,并引入一个新的权重参数,该参数用来描述在当前时间步如何使用上一时间步的隐藏变量。
所以,当前时间步隐藏变量的计算需要由当前输入以及上一个时间步输出共同确定:

每个时间步t,输出的结果为:

RNN - 图1
来模拟一下:

  1. import torch
  2. X, W_xh = torch.randn(3, 1), torch.randn(1, 4)
  3. H, W_hh = torch.randn(3, 4), torch.randn(4, 4)
  4. print(torch.matmul(X, W_xh) + torch.matmul(H, W_hh))
  1. 这个运算实际上也等同于与连结后的矩阵乘以与 连结后的矩阵:
  1. import torch
  2. X, W_xh = torch.randn(3, 1), torch.randn(1, 4)
  3. H, W_hh = torch.randn(3, 4), torch.randn(4, 4)
  4. print(torch.matmul(X, W_xh) + torch.matmul(H, W_hh))
  5. print(torch.matmul(torch.cat((X, H), dim=1), torch.cat((W_xh, W_hh), dim=0)))
  6. 输出:
  7. tensor([[ 3.8779, -1.6057, 1.1188, 1.4510],
  8. [ 0.1335, 0.0381, -0.7219, 2.6631],
  9. [-2.3299, 1.6547, -4.4616, -3.0346]])
  10. tensor([[ 3.8779, -1.6057, 1.1188, 1.4510],
  11. [ 0.1335, 0.0381, -0.7219, 2.6631],
  12. [-2.3299, 1.6547, -4.4616, -3.0346]])

数据处理

随机采样

每次从数据里随机采样一个小批量。其中批量大小batch_size指每个小批量的样本数,num_steps为每个样本所包含的时间步数。 在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相毗邻。因此,我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。在训练模型时,每次随机采样前都需要重新初始化隐藏状态。

相邻采样

除对原始序列做随机采样之外,我们还可以令相邻的两个随机小批量在原始序列上的位置相毗邻。这时候,我们就可以用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态,从而使下一个小批量的输出也取决于当前小批量的输入,并如此循环下去。这对实现循环神经网络造成了两方面影响:一方面, 在训练模型时,我们只需在每一个迭代周期开始时初始化隐藏状态;另一方面,当多个相邻小批量通过传递隐藏状态串联起来时,模型参数的梯度计算将依赖所有串联起来的小批量序列。同一迭代周期中,随着迭代次数的增加,梯度的计算开销会越来越大。 为了使模型参数的梯度计算只依赖一次迭代读取的小批量序列,我们可以在每次读取小批量前将隐藏状态从计算图中分离出来。

从零实现

先手动实现一个构造one hot向量的函数。这里要注意,需要先对tensor的形状做一些处理。一般来说,输入的序列形状一般为(batch大小,序列长度)。数据集里所有序列长度都是一个定值,因为书中的做法是将歌词全部拼接起来,然后再切分成指定大小的段。
在进行转换时,我们需要输出形状为(batch大小,词典大小)的张量,然后根据在序列中的下标塞进一个数组里。如[第一个时间步的tensor,第二个时间步的tensor,第三个时间步的tensor…]

  1. def one_hot(x, n_class, dtype=torch.float32):
  2. x = x.long()
  3. res = torch.zeros(x.shape[0], n_class, dtype=dtype)
  4. res.scatter_(1, x.view(-1, 1), 1)
  5. return res
  6. def to_onehot(x, n_class):
  7. return [one_hot(x[:, i], n_class) for i in range(x.shape[1])]
  1. 其他细节省略,参考[d2l](https://tangshusen.me/Dive-into-DL-PyTorch/#/chapter06_RNN/6.4_rnn-scratch?id=_642-%e5%88%9d%e5%a7%8b%e5%8c%96%e6%a8%a1%e5%9e%8b%e5%8f%82%e6%95%b0)
  1. import math
  2. import torch
  3. from torch import nn
  4. import time
  5. import d2lzh
  6. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  7. corpus_indices, char_to_idx, idx_to_char, vocab_size = d2lzh.load_data_jay_lyrics()
  8. def one_hot(x, n_class, dtype=torch.float32):
  9. x = x.long()
  10. res = torch.zeros(x.shape[0], n_class, dtype=dtype, device=device)
  11. res.scatter_(1, x.view(-1, 1), 1)
  12. return res
  13. def to_onehot(x, n_class):
  14. return [one_hot(x[:, i], n_class) for i in range(x.shape[1])]
  15. num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
  16. def get_params():
  17. def _norm(shape):
  18. return torch.nn.Parameter(torch.normal(0, 0.01, shape, device=device), requires_grad=True)
  19. def _zero(shape):
  20. return torch.nn.Parameter(torch.zeros(shape, device=device), requires_grad=True)
  21. W_xh = _norm((num_inputs, num_hiddens))
  22. W_hh = _norm((num_hiddens, num_hiddens))
  23. B_h = _zero(num_hiddens)
  24. W_hq = _norm((num_hiddens, num_outputs))
  25. B_q = _zero(num_outputs)
  26. return nn.ParameterList([W_xh, W_hh, B_h, W_hq, B_q])
  27. def init_rnn_state(batch_size, num_hiddens):
  28. return torch.zeros((batch_size, num_hiddens), device=device)
  29. def rnn(inputs, state, params):
  30. W_xh, W_hh, B_h, W_hq, B_q = params
  31. H = state
  32. outputs = []
  33. for input in inputs:
  34. H = torch.tanh(torch.matmul(input, W_xh) + torch.matmul(H, W_hh) + B_h)
  35. outputs.append(torch.matmul(H, W_hq) + B_q)
  36. return outputs, H
  37. def predict_rnn(prefix, num_chars, params):
  38. state = init_rnn_state(1, num_hiddens)
  39. outputs = [char_to_idx[prefix[0]]]
  40. for step in range(len(prefix) + num_chars - 1):
  41. X = to_onehot(torch.tensor([[outputs[-1]]], device=device), vocab_size)
  42. output, state = rnn(X, state, params)
  43. if step < len(prefix) - 1:
  44. outputs.append(char_to_idx[prefix[step + 1]])
  45. else:
  46. outputs.append(int(output[0].argmax(dim=1).item()))
  47. return ''.join([idx_to_char[i] for i in outputs])
  48. def train_and_predict_rnn(is_random_iter, num_epochs, num_steps,
  49. lr, clipping_theta, batch_size, pred_period,
  50. pred_len, prefixes):
  51. params = get_params()
  52. if is_random_iter:
  53. data_iter_fn = d2lzh.data_iter_random
  54. else:
  55. data_iter_fn = d2lzh.data_iter_consecutive
  56. loss = nn.CrossEntropyLoss()
  57. for epoch in range(num_epochs):
  58. data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
  59. if not is_random_iter:
  60. state = init_rnn_state(batch_size, num_hiddens)
  61. l_sum, n, start = 0.0, 0, time.time()
  62. for X, Y in data_iter:
  63. if is_random_iter:
  64. state = init_rnn_state(batch_size, num_hiddens)
  65. else:
  66. state.detach_()
  67. inputs = to_onehot(X, vocab_size)
  68. outputs, state = rnn(inputs, state, params)
  69. outputs = torch.cat(outputs, dim=0)
  70. y = torch.transpose(Y, 0, 1).contiguous().view(-1) # 必须加上contiguous,因为transpose操作底层tensor并不改变
  71. l = loss(outputs, y.long())
  72. l.backward()
  73. d2lzh.grad_clipping(params, clipping_theta, device)
  74. for param in params:
  75. param.data -= param.grad*lr
  76. # 梯度清零操作放在loss和back中间或者sgd下面都是可以的
  77. for param in params:
  78. param.grad.data.zero_()
  79. l_sum += l.item() * y.shape[0]
  80. n += y.shape[0]
  81. if (epoch + 1) % pred_period == 0:
  82. print('epoch %d, perplexity %f, time %.2f sec' % (
  83. epoch + 1, math.exp(l_sum / n), time.time() - start))
  84. for prefix in prefixes:
  85. print(' -', predict_rnn(prefix, pred_len, params))
  86. if __name__ == '__main__':
  87. params = get_params()
  88. num_epochs, num_steps, batch_size, lr, clipping_theta = 300, 35, 32, 1e2, 1e-2
  89. pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
  90. train_and_predict_rnn(True, num_epochs, num_steps, lr, clipping_theta, batch_size, pred_period, pred_len, prefixes)

简洁实现

从零实现时我们搭建的RNN,输入形状为(批量大小,词典大小)的张量。但在调包时,rnn_layer的输入形状为(时间步数,批量大小,输入个数),输出则是各个时间步上计算得出的隐藏状态。该输出与我们之前实现的也不同,因为它仅仅是一个隐藏状态,不包括输出层的计算,形状为(时间步数,批量大小,隐藏单元数)。nn.RNN实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中。
测试一下:

  1. import math
  2. import torch
  3. from torch import nn, optim
  4. import torch.nn.functional as F
  5. import time
  6. import d2lzh
  7. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  8. corpus_indices, char_to_idx, idx_to_char, vocab_size = d2lzh.load_data_jay_lyrics()
  9. num_hiddens = 256
  10. rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=num_hiddens)
  11. num_steps = 35
  12. batch_size = 5
  13. X = torch.randn(35, 5, vocab_size)
  14. state = None
  15. Y, state = rnn_layer(X, state)
  16. print(Y.shape)
  17. print(state.shape)
  18. 输出:
  19. torch.Size([35, 5, 256])
  20. torch.Size([1, 5, 256])
  1. 定义模型:
  1. import math
  2. import torch
  3. from torch import nn, optim
  4. import torch.nn.functional as F
  5. import time
  6. import d2lzh
  7. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  8. corpus_indices, char_to_idx, idx_to_char, vocab_size = d2lzh.load_data_jay_lyrics()
  9. class RNNModel(nn.Module):
  10. def __init__(self, rnn_layer, vocab_size):
  11. super().__init__()
  12. self.rnn_layer = rnn_layer
  13. self.hidden_size = rnn_layer.hidden_size
  14. self.vocab_size = vocab_size
  15. self.dense = nn.Linear(self.hidden_size, self.vocab_size)
  16. def forward(self, inputs, state):
  17. X = torch.stack(d2lzh.to_onehot(inputs, self.vocab_size))
  18. Y, state = self.rnn_layer(X, state)
  19. # 将输出形状变为(num_steps * batch_size, vocab_size)
  20. output = self.dense(Y.view(-1, Y.shape[-1]))
  21. return output, state
  22. num_steps, batch_size = 35, 2
  23. rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=512)
  24. rnn = RNNModel(rnn_layer, vocab_size)
  25. Y, state = rnn(torch.tensor([[1, 2, 3], [1, 2, 3]]), None)
  26. print(Y.shape)
  1. 训练:
  1. import math
  2. import torch
  3. from torch import nn, optim
  4. import torch.nn.functional as F
  5. import time
  6. import d2lzh
  7. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  8. corpus_indices, char_to_idx, idx_to_char, vocab_size = d2lzh.load_data_jay_lyrics()
  9. class RNNModel(nn.Module):
  10. def __init__(self, rnn_layer, vocab_size):
  11. super().__init__()
  12. self.rnn_layer = rnn_layer
  13. self.hidden_size = rnn_layer.hidden_size
  14. self.vocab_size = vocab_size
  15. self.dense = nn.Linear(self.hidden_size, self.vocab_size)
  16. def forward(self, inputs, state):
  17. X = torch.stack(d2lzh.to_onehot(inputs, self.vocab_size))
  18. Y, state = self.rnn_layer(X, state)
  19. # 将输出形状变为(num_steps * batch_size, vocab_size)
  20. output = self.dense(Y.view(-1, Y.shape[-1]))
  21. return output, state
  22. num_steps, num_hiddens = 35, 512
  23. rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=512)
  24. model = RNNModel(rnn_layer, vocab_size).to(device)
  25. print(d2lzh.predict_rnn_pytorch('分开', num_steps, model, vocab_size, device, idx_to_char, char_to_idx))
  26. num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e-3, 1e-2 # 注意这里的学习率设置
  27. pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
  28. d2lzh.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
  29. corpus_indices, idx_to_char, char_to_idx,
  30. num_epochs, num_steps, lr, clipping_theta,
  31. batch_size, pred_period, pred_len, prefixes)

梯度裁剪

关于为什么进行梯度裁剪,详见https://tangshusen.me/Dive-into-DL-PyTorch/#/chapter06_RNN/6.6_bptt