循环神经网络
序列模型
序列数据
-
实际中很多数据是有时序结构的
-
比如:电影的评价随时间变化而变化
- 拿奖后评分上升,直到奖项被忘记
- 看了很多好电影后,人们的期望变高
- 季节性:贺岁片、暑期档
- 导演、演员的负面报道导致评分变低
核心思想:一个电影出来之后,大家对该电影的评价打分,不仅仅和电影质量有关,也和很多因素有关,会跟着时间变化而变化。
-
更多例子:
- 音乐、语言、文本、和视频都是连续的
- 标题 “狗咬人” 远没有 “人咬狗” 那么令人惊讶
- 大地震发生后,很可能会有几次较小的余震
- 人的互动是连续的,从网上吵架可以看出
- 预测明天的股价要比填补昨天遗失的股价的更困难(有大量随机性)
统计工具
如何对以上的数据进行建模?
- 在时间 t 观察到 xt,那么得到 T 个不独立的随机变量(x1,…xT) ~ p(x)
- 使用条件概率展开:p(a,b)= p(a)p(b |a)= p(b)p(a|b)
根据以上可以得到p(x):
如果想要算xT的话,我们需要知道过去发生的事情(之前所有(T-1)
也可以反过来进行写(反序):
我已经知道未来的事情,推之前发生的事情(物理上不一定可行)
序列模型
- 对条件概率建模:
在过去T-1数据上建立一个模型,然后预测第T个数据。(label 和 真实的样本是一个东西 --> 这样的模型叫自回归模型)
核心思想: 如何计算f(x1, …, x t-1)
解决:
方案A - 马尔科夫假设
- 假设当前数据只跟 τ 个过去数据点相关
当时间范围很长的时候,需要看很多东西,马尔科夫假设:只需要看过去的 τ 个。建模如下:
可以使用线性回归和MLP模型对f建模来解决。
方案 B-潜变量模型
- 引人潜变量h_t来表示过去信息 h_t = f(x1,……x_(t-1))
这样,可得到:
h_t可看作一个向量或者一个标量,用一个变量来表示前面所有时刻数据的信息。
总结
- 时序模型中,当前数据跟之前观察到的数据相关
- 自回归模型使用自身过去数据来预测未来
- 马尔科夫模型假设当前只跟最近少数数据相关从而简化模型
- 潜变量模型使用潜变量来概括历史信息
代码实现
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l# 使用正弦函数和一些可加性噪声来生成序列数据, 时间步为 1,2,…,1000
T = 1000 # 一共产生1000个点
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
d2l.plot(time, [x], 'time', 'x', xlim=[1, 100], figsize=(6, 3))# 接下来, 将这个序列转换为模型的特征-标签(feature-label)对。
# 基于嵌入维度 𝜏 ,我们[将数据映射为数据对 𝑦𝑡=𝑥𝑡 和 𝐱𝑡=[𝑥𝑡−𝜏,…,𝑥𝑡−1]
tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):features[:, i] = x[i: T - tau + i]
labels = x[tau:].reshape((-1, 1))batch_size, n_train = 16, 600
# 只有前n_train个样本用于训练
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),batch_size, is_train=True)
train_iter# 使用一个相当简单的架构训练模型: 一个拥有两个全连接层的多层感知机
# ReLU激活函数和平方损失。# 初始化网络权重的函数
def init_weight(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)# 一个简单的多层感知机
def get_net():net = nn.Sequential(nn.Linear(4, 10),nn.ReLU(),nn.Linear(10, 1))net.apply(init_weight)return net# 平方损失。注意:MSELoss计算平方误差时不带系数1/2
loss = nn.MSELoss(reduction='none')# 训练模型
def train(net, train_iter, loss, epochs, lr):trainer = torch.optim.Adam(net.parameters(), lr)for epoch in range(epochs):for X, y in train_iter:trainer.zero_grad()l = loss(net(X), y)l.sum().backward()trainer.step()print(f'epoch {epoch + 1},'f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')net = get_net()
train(net, train_iter, loss, 5, 0.01)# 预测
# 模型预测下一个时间步 (单步预测)onestep_preds = net(features)
d2l.plot([time, time[tau:]],[x.detach().numpy(), onestep_preds.detach().numpy()], 'time','x',legend=['data', '1-step preds'], xlim=[1, 1000],figsize=(6, 3))# 进行多步预测
multistep_preds = torch.zeros(T)
multistep_preds[: n_train + tau] = x[: n_train + tau]
for i in range(n_train + tau, T):multistep_preds[i] = net(multistep_preds[i-tau:i].reshape((1, -1)))d2l.plot([time, time[tau:], time[n_train + tau:]],[x.detach().numpy(), onestep_preds.detach().numpy(),multistep_preds[n_train + tau:].detach().numpy()], 'time','x', legend=['data', '1-step preds', 'multistep preds'],xlim=[1, 1000], figsize=(6, 3))# 以上预测结果并不理想
# 𝑘=1,4,16,64 ,通过对整个序列预测的计算, 让我们[更仔细地看一下𝑘步预测]的困难
max_steps = 64
features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# 列i(i<tau)是来自x的观测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(tau):features[:, i] = x[i: i + T - tau - max_steps + 1]# 列i(i>=tau)是来自(i-tau+1)步的预测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(tau, tau + max_steps):features[:, i] = net(features[:, i - tau:i]).reshape(-1)steps = (1, 4, 16, 64)
d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps],[features[:, tau + i - 1].detach().numpy() for i in steps], 'time', 'x',legend=[f'{i}-step preds' for i in steps], xlim=[5, 1000],figsize=(6, 3))
预测结果:
预测1个点和4个点、16个点都还可以,但是64个点就差了。
文本预处理
-
文本预处理的重要性:
-
文本数据通常是非结构化的,需要转换为数值形式才能被神经网络处理。
-
文本预处理包括分词、构建词汇表、将词转换为索引等步骤。
-
-
分词(Tokenization):
-
可以将文本按单词(token=‘word’)或字符(token=‘char’)进行拆分。按单词拆分时,使用字符串的split方法;按字符拆分时,直接将字符串转换为字符列表。
-
分词方法可以根据语言和任务的不同而有所变化。
-
-
构建词汇表(Vocabulary):
-
词汇表是所有唯一词的集合,每个词被赋予一个唯一的索引。
-
词汇表的大小会影响模型的复杂度和性能。
-
-
词索引转换:
-
将分词后的文本转换为索引序列,以便输入到神经网络中。
-
可以使用特殊的索引来表示未知词(unknown words)或填充(padding)。
-
-
加载数据集:
- 使用PyTorch的Dataset和DataLoader来加载和批处理文本数据。
- 例如:使用read_time_machine函数从 H.G.Well 的《时光机器》中加载文本数据。该函数利用d2l.download下载数据集文件,再通过re.sub函数去除文本中的非字母字符,将文本转换为小写并去除首尾空格,最后以列表形式返回处理后的文本行。
代码实现
# 文本预处理# 将解析文本的常见预处理步骤。 这些步骤通常包括:# 将文本作为字符串加载到内存中。
# 将字符串拆分为词元(如单词和字符)。
# 建立一个词表,将拆分的词元映射到数字索引。
# 将文本转换为数字索引序列,方便模型操作。import collections
import re
from d2l import torch as d2l# 读取数据
# 从 H.G.Well的时光机器中加载文本
# 这是一个相当小的语料库,只有30000多个单词
# 现实中的文档集合可能会包含数十亿个单词# 下面的函数:将数据集读取到由多条文本行组成的列表中 (其中每条文本行都是一个字符串)
# 为简单起见, 忽略了标点符号和字母大写。
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt','090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine():"""将时间机器数据集加载到文本行的列表中"""with open(d2l.download('time_machine'), 'r') as f:lines = f.readlines()return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]lines = read_time_machine()
print(f'# 文本总行数:{len(lines)}')
print(lines[0])
print(lines[10])# 词元化
# tokenize函数将文本行列表(lines)作为输入, 列表中的每个元素是一个文本序列(如一条文本行)
# 每个文本序列又被拆分成一个词元列表,词元(token)是文本的基本单位
def tokenize(lines, token='word'):"""将1文本拆分为单词或字符词元"""if token == 'word':return [line.split() for line in lines]elif token == 'char':return [list(line) for line in lines]else:print('错误:未知词元类型:' + token)tokens = tokenize(lines)
for i in range(11):print(tokens[i])# 词表
# 词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。
# 构建一个字典,通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从 0 开始的数字索引中
# 我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计, 得到的统计结果称之为语料(corpus)
# 根据每个唯一词元的出现频率,为其分配一个数字索引。
# 很少出现的词元通常被移除,这可以降低复杂性
# 语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“<unk>”
# 可以选择增加一个列表,用于保存那些被保留的词元, 例如:填充词元(“<pad>”); 序列开始词元(“<bos>”); 序列结束词元(“<eos>”)。class Vocab:"""文本词表"""def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):# 初始化词表# tokens: 输入的词元列表,可以说1D列表也可以是2D列表# min_freq: 词元的最小出现频率,低于此频率的词元将被忽略# reserved_tokens: 保留的词元列表,如填充词元<pad>、序列开始词元<bos>、序列结束词元<eos>等if tokens is None:tokens = [] # 如果tokens为空,初始化为空列表if reserved_tokens is None:reserved_tokens = [] # 如果reserved_tokens为空,初始化为空列表# 按出现频率排序# 统计词元的频率counter = count_corpus(tokens)# 按词元频率从高到低排序self._token_freqs = sorted(counter.items(), key=lambda x: x[1],reverse=True)# 初始化词元到索引的映射# 未知词元的索引为0self.idx_to_token = ['<unk>'] + reserved_tokens# 创建词元到索引的字典self.token_to_idx = {token: idxfor idx, token in enumerate(self.idx_to_token)}# 将频率高于min_freq的词元加入词表for token, freq in self._token_freqs:if freq < min_freq:break # 如果词元频率低于min_freq,停止添加if token not in self.token_to_idx:# 将词元加入idx_to_token列表,并更新token_to_idx字典self.idx_to_token.append(token)self.token_to_idx[token] = len(self.idx_to_token) - 1def __len__(self):# 返回词表的大小return len(self.idx_to_token)def __getitem__(self, tokens):# 获取词元的索引# 如果tokens是单个词元,返回其索引;如果是列表或元组,返回索引列表if not isinstance(tokens, (list, tuple)):return self.token_to_idx.get(tokens, self.unk)# 如果词元不在词表中,返回未知词元的索引return [self.__getitem__(token) for token in tokens]def to_tokens(self, indices):# 将索引转换为词元# 如果indices是单个索引,返回对应的词元;如果是列表或元组,返回词元列表if not isinstance(indices, (list, tuple)):return self.idx_to_token[indices]return [self.idx_to_token[index] for index in indices]@propertydef unk(self): # 未知词元的索引为0# 返回未知词元的索引(固定为0)return 0@propertydef token_freqs(self):# 返回词元的频率列表return self._token_freqsdef count_corpus(tokens): #@save"""统计词元的频率"""# 这里的tokens是1D列表或2D列表# 如果tokens是2D列表(如多行文本),将其展平为1D列表if len(tokens) == 0 or isinstance(tokens[0], list):# 将词元列表展平成一个列表tokens = [token for line in tokens for token in line]# 使用collections.Counter统计词元频率return collections.Counter(tokens)# 以上代码的使用场景
# 该词表类通常用于自然语言处理(NLP)任务中,将文本数据转换为模型可以处理的数字形式。
#通过构建词表,可以将文本中的每个词元映射到一个唯一的索引,方便后续的模型训练和推理。# 使用时光机器数据集作为语料库来构建词表,然后打印前几个高频词元及其索引
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])# 下面,将每条文本行转换成一个数字索引列表
for i in [0, 10]:print('文本:', tokens[i])print('索引:', vocab[tokens[i]])# 整合所有功能
# 将所有功能打包到load_corpus_time_machine函数中,该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表)def load_corpus_time_machine(max_tokens=-1): #@save"""返回时光机器数据集的词元索引列表和词表"""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]return corpus, vocabcorpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)
总结
- 文本是序列数据的一种最常见的形式之一。
- 为了对文本进行预处理,我们通常将文本拆分为词元,构建词表将词元字符串映射为数字索引,并将文本数据转换为词元索引以供模型操作。
语言模型
- 给定文本序列x1,…,xT,语言模型的目标是估计联合概率p(x1,…,xT)
复习知识:在概率论中,联合概率是指在多元的概率分布中多个随机变量分别满足各自条件的概率。它表示两个或多个事件同时发生的可能性。
- 应用包括:
- 预训练模型(BERT、GPT、deep seek)
- 生成文本,给定前面几个词,不断使用xt ~ p(xt | x1, …, x t-1)来生成后续文本
- 判断多个序列中哪个更常见。
使用计数来建模(简单模型)
- 假设序列长度为2,预测
n:总词数,n(x),n(x,x’)是单个单词和连续单词对的出现次数。 - 拓展到长为3的情况:
带来的问题:就是序列变得很长(马尔可夫假设、潜变量)
N元语法
- 当序列很长时,因为文本量不够大,很可能 n(x1,…,xT)≤1
- 使用马尔科夫假设可以缓解这个问题:
- 这样处理的好处:可以处理比较长的序列;但随着N的增大也会存在序列太长的情况。
总结
- 语言模型估计文本序列的联合概率
- 使用统计方法时常采用n元语法
代码实现 - 数据处理
# 语言模型和数据集
import random
import torch
from d2l import torch as d2l# 读取《时间机器》的文本。
tokens = d2l.tokenize(d2l.read_time_machine())
# 对文本进行分词:将分词后的结果展平成一个一维列表 corpus,其中每个元素是一个单词或符号。
corpus = [token for line in tokens for token in line]
# 构建一个词汇表,统计每个单词的频率。
vocab = d2l.Vocab(corpus)
# 查看频率最高的前10个单词及其频率。
vocab.token_freqs[:10]# 做流行的词 被称为 停用词 ,画出的词频图:
freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log')# 其他词元组合:比如二元语法、三元语法等
# corpus:是之前构建的一维列表,包含所有单词或符号。
# corpus[:-1]:表示从第一个单词到倒数第二个单词的列表。
# corpus[1:]:表示从第二个单词到最后一个单词的列表。
# zip(corpus[:-1], corpus[1:]):将 corpus[:-1] 和 corpus[1:] 组合成一个迭代器,生成相邻单词对(即二元语法对)。
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]# 这里值得注意:在十个最频繁的词对中,有九个是由两个停用词组成的, 只有一个与“the time”有关。
# 我们再进一步看看三元语法的频率是否表现出相同的行为方式。# 构建三元语法
trigram_tokens = [triple for triple in zip(corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)
trigram_vocab.token_freqs[:10]# 直观地对比三种模型中的词元频率]:一元语法、二元语法和三元语法。bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs]
d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x',ylabel='frequency: n(x)', xscale='log', yscale='log',legend=['unigram', 'bigram', 'trigram'])# 总结:
# 自然语言中的单词和单词序列都遵循齐普夫定律,但单词序列的频率分布更加平缓。# 拉普拉斯:
# 由于语言中存在规律性,实际有效的 𝑛 元组数量并没有爆炸式增长,这为建模提供了可能性。# 传统的平滑方法(如拉普拉斯平滑)在处理 𝑛 元组时效果不佳,因为低频和未出现的 𝑛 元组数量太多。# 基于深度学习的模型可以更好地捕捉语言的结构和规律,因此是更合适的语言建模工具。# 随机采样:每个样本都是在原始的长序列上任意捕获的子序列
# 随机生成一个小批量数据的特征和标签以供读取。在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列# 参数batch_size指定了每个小批量中子序列样本的数目, 参数num_steps是每个子序列中预定义的时间步数。
def seq_data_iter_random(corpus, batch_size, num_steps):"""使用随机抽样生成一个小批量子序列"""# 从语料库的随机位置开始,随机位置的范围是从 0 到 num_steps - 1# 这样做是为了增加数据的随机性,避免每次训练都从相同的位置开始corpus = corpus[random.randint(0, num_steps - 1):]# 计算语料库中可以生成的子序列的数量# 由于每个子序列的长度为 num_steps,且目标序列是子序列向后移动一个时间步,所以需要减去 1num_subseqs = (len(corpus) - 1) // num_steps# 生成每个子序列的起始索引# 这些索引是等间隔的,间隔为 num_stepsinitial_indices = list(range(0, num_subseqs * num_steps, num_steps))# 随机打乱起始索引的顺序,这样可以保证每次抽样的子序列是随机的random.shuffle(initial_indices)# 定义一个内部函数 data,用于从语料库中提取指定位置开始的长度为 num_steps 的子序列def data(pos):return corpus[pos:pos + num_steps]# 计算可以生成的批量数量num_batches = num_subseqs // batch_sizefor i in range(0, batch_size * num_batches, batch_size):# 从打乱后的起始索引中选取当前批量的起始索引initial_indices_per_batch = initial_indices[i: i + batch_size]# 提取当前批量的输入序列 X# 对于每个起始索引 j,调用 data 函数提取长度为 num_steps 的子序列X = [data(j) for j in initial_indices_per_batch]# 提取当前批量的目标序列 Y# 目标序列是输入序列向后移动一个时间步,所以起始位置为 j + 1: X序列的后一个Y = [data(j + 1) for j in initial_indices_per_batch]# 使用生成器返回当前批量的输入序列 X 和目标序列 Y# 生成器可以逐个返回批量数据,节省内存yield torch.tensor(X), torch.tensor(Y)# 验证:[生成一个从 0 到 34 的序列]。
# 假设批量大小为 2 ,时间步数为 5 ,这意味着可以生成 ⌊(35−1)/5⌋=6 个“特征-标签”子序列对。
# 如果设置小批量大小为 2 ,我们只能得到 3 个小批量。my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):print('X: ', X, '\nY:', Y)# 顺序分区
# 在迭代过程中,除了对原始序列可以随机抽样外, 我们还可以[保证两个相邻的小批量中的子序列在原始序列上也是相邻的]
def seq_data_iter_sequential(corpus, batch_size, num_steps):"""使用顺序分区生成一个小批量子序列"""# 1. 随机选择一个偏移量# 从 0 到 num_steps 之间随机选择一个整数作为偏移量,目的是增加数据的随机性,避免每次都从语料库的固定位置开始offset = random.randint(0, num_steps)# 2. 计算可用于生成批量数据的令牌数量# 首先,(len(corpus) - offset - 1) 表示从语料库中去除偏移量和最后一个可能不完整的子序列后剩余的长度# 然后,将其除以 batch_size 并向下取整,再乘以 batch_size,确保得到的令牌数量是 batch_size 的整数倍,方便后续按批量处理num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size# 3. 提取输入序列 Xs 和目标序列 Ys# Xs 是从语料库中偏移 offset 位置开始,取 num_tokens 个元素作为输入序列Xs = torch.tensor(corpus[offset: offset + num_tokens])# Ys 是从语料库中偏移 offset + 1 位置开始,取 num_tokens 个元素作为目标序列,即 Ys 是 Xs 向后移动一个时间步得到的Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])# 4. 调整 Xs 和 Ys 的形状# 将 Xs 和 Ys 重塑为形状为 (batch_size, -1) 的二维张量,其中 -1 表示该维度的大小由张量的总元素数和其他维度的大小自动推断得出# 这样做是为了将数据按批量进行组织Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)# 5. 计算批量的数量# 通过 Xs 张量的第二维大小(即每个批量包含的元素总数)除以每个子序列的长度 num_steps,得到可以生成的批量数量num_batches = Xs.shape[1] // num_steps# 6. 遍历每个批量# 使用 for 循环,从 0 开始,以 num_steps 为步长,遍历到 num_steps * num_batches,依次取出每个批量的数据for i in range(0, num_steps * num_batches, num_steps):# 提取当前批量的输入序列 X# 从 Xs 中取出第 i 到 i + num_steps 列的数据,即每个样本的长度为 num_stepsX = Xs[:, i: i + num_steps]# 提取当前批量的目标序列 Y# 从 Ys 中取出第 i 到 i + num_steps 列的数据,与输入序列 X 对应Y = Ys[:, i: i + num_steps]# 使用生成器逐个返回当前批量的输入序列 X 和目标序列 Y,节省内存yield X, Y# 通过顺序分区[读取每个小批量的子序列的特征X和标签Y]
# 迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。
for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):print('X: ', X, '\nY:', Y)# 将上面的两个采样函数包装到一个类中], 以便稍后可以将其用作数据迭代器。
class SeqDataLoader:"""加载序列数据的迭代器"""def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):if use_random_iter:self.data_iter_fn = d2l.seq_data_iter_randomelse:self.data_iter_fn = d2l.seq_data_iter_sequentialself.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)self.batch_size, self.num_steps = batch_size, num_stepsdef __iter__(self):return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)# 最后,我们定义了一个函数load_data_time_machine, 它同时返回数据迭代器和词表],
# 因此可以与其他带有load_data前缀的函数 (如 :numref:sec_fashion_mnist中定义的 d2l.load_data_fashion_mnist)类似地使用。def load_data_time_machine(batch_size, num_steps, #@saveuse_random_iter=False, max_tokens=10000):"""返回时光机器数据集的迭代器和词表"""data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens)return data_iter, data_iter.vocab
循环神经网络
对于序列模型的神经网络就是:循环神经网络;
回忆:潜变量自回归模型
- 使用潜变量ht总结过去的信息。
循环神经网络
- 更新隐藏状态:
h_t = f(W_hh * h_t-1 + W_hx * x_t + b_h)
(去掉W_hh * h_t-1 就变成了MLP) - 输出:o_t = W_ho * h_t + bo
如下图:使用循环神经网络的语言模型
所以说,在循环卷积网络中,当前神经元的输出只与xt-1以及ht-1有关,然后计算损失的时候,是将ot与xt进行比较,因为模型的下一个输出就是序列的下一个。
补充:感觉这个图更加生动一些(来源:B站耿直哥)
困惑度(perplexity)
- 衡量一个语言模型的好坏可以使用平均交叉熵。
p是语言模型的预测概率,x是真实词 - 历史原因NLP使用困惑度 exp(z)来衡量,是平均每次可能选项
- 1表示完美,无穷大是最差情况
梯度剪裁
- 迭代中计算这T个时间步上的梯度,在反向传播过程中产生长度为 0(T)的矩阵乘法链,导致数值不稳定
- 梯度裁剪能有效预防梯度爆炸
- 如果梯度长度超过θ,那么拖影回长度θ
- 如果梯度长度超过θ,那么拖影回长度θ
应用
总结
- 循环神经网络的输出取决于当下输入和前一时间的隐变量(过去的历史信息)
- 拿到过去的输入和当前的隐藏状态 --> 可得到下一个输出。
- 应用到语言模型中时,循环神经网络根据当前词预测下一次时刻词
- 通常使用困惑度来衡量语言模型的好坏。
代码实现-循环神经网络的简洁实现
# 循环神经网络的简洁实现import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 定义模型
# 高级API提供了循环神经网络的实现。
# 构造一个具有256个隐藏单元的单隐藏层的循环神经网络层rnn_layernum_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# 为一个完整的循环神经网络模型定义了一个RNNModel类
class RNNModel(nn.Module):"""循环神经网络模型"""def __init__(self, rnn_layer, vocab_size, **kwargs):super(RNNModel, self).__init__(**kwargs)self.rnn = run_layerself.vocab_size = vocab_sizeself.num_hiddens = self.rnn.hidden_size# 如果RNN是双向的(之后将介绍),num_directions应该是2, 否则应该是1if not self.rnn.bidirectional:self.num_directions = 1self.linear = nn.Linear(self.num_hiddens, self.vocab_size)else:self.num_directions = 2self.linear = nn.Linear(self.num_hiddens, 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, statedef 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))# 模型训练与预测
# 基于一个具有随机权重的模型进行预测
device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
d2l.predict_ch8('time traveller', 10, net, vocab, device)num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)
结果如下: