[TOC]
updated: 2024-4-13 11:17:31
卷积神经网络可以有效地处理空间信息,循环神经网络可以更好地处理序列信息。通过引入记忆单元存储过去的信息,结合当前的输入,确定当前的输出
8.1 序列模型
8.1.1 序列数据
数据间是有时序结构的,
对当前结果的预测不仅受当前输入数据影响,历史数据也会对当下、未来数据产生影响,但历史数据的重要性随时间会衰减
序列数据例子
归纳偏置——动力学静止
对于一个特定的序列任务中,虽然特定的随机变量 $x_t$ 会改变,但 序列本身的动力学(数据的时序结构)不会改变。在统计学上称不变的动力学为 静止的,所以是可以学习的
动力学:序列数据中的动力学指数据点随时间的变化规律
一个时间序列被认为是动力学静止的,如果它的 统计属性不随时间变化(如均值、方差和自协方差)
静止的时间序列是一个没有趋势、季节性或周期性的 随机过程 ,每个时间点的观测值都是随机的,并且与其他时间点的观测值有着相同的统计特性
动力学静止的关键特征 ——自协方差独立性:时间序列的任意两个时期的观测值之间的协方差只依赖于它们之间的时间间隔,而不是具体的时间点
类似于自然语言中的语法,一句话中,一个单词的下一个单词可能是词表中的任意一个词,但由于二元语法、三元语法、多元语法的存在,下一个位置可能出现的单词数量会比词表大小少很多
- 序列中,下一时间步可能的状态也是无穷的,但真正出现的状态是有一定规律的,也就是只会出现有限种状态,所以动力学静止的序列可以通过学习找到这个规律
如果时间序列数据不是静止的,通常需要进行转换以使其静止
若在时刻 $t$ 有观测值 $x_t$ ,可以得到 $T$ 个与时间相关的不独立随机变量 $x_1,x_2,\cdots,x_T$
因此整个序列的估计值可以通过全概率公式得到
即计算 $T$ 时刻的输出 $x_T$ ,需要结合历史数据序列 $\{x_1,x_2,\cdots,x_{T-1}\}$
也可以反向计算:根据未来数据倒退过去数据
8.1.2 序列模型
为了利用历史信息处理时序数据,需要让神经网络具有记忆能力
延时神经网络(TDNN)
利用历史信息建立一个额外的延时(记忆)单元,用于存储网络的历史信息(输入、输出、隐状态)
在 $t$ 时刻,第 $l$ 层神经元的活性值依赖于第 $l-1$ 层的同时刻输出及其相邻的 $U$ 个历史信息
延时神经网络在同一层的不同时间步共享权值,以降低参数数量。
自回归模型
我们可以对一个对象的历史数据建模,使其能拟合自身,因为标签为数据本身,所以称为 自回归模型
- 若数据是离散的,则 $f()$ 为分类器
- 若数据是连续的,则 $f()$ 为回归模型
自回归模型 :AutoRegressive Model(AR)
其中 $U$ 为表示时序窗口大小的超参数,$\mathbf{W}_0,\mathbf{W}_1,\cdots,\mathbf{W}_U$ 为可学习参数,$\varepsilon_t\sim \mathcal{N}(0,\sigma^2)$ 为第 $t$ 时刻的噪声
有外部输入的非线性自回归模型 :Nonlinear AutoRegressive with Exogenous Inputs Model(NARX)
其中,$f(\cdot)$ 表示非线性函数,$U_x,U_h$ 是表示时序窗口的超参数
马尔科夫假设
当前数据只与 $\tau$ 个过去数据点相关
即自回归模型有 $\tau$ 个连续的输入数据,将变长序列变为定长的序列
直接好处是参数的数量不变
潜变量模型
引入潜变量 $h_t$ 表示随机变量的时序结构(隐变量:未观测到的变量;潜变量:可能不存在的变量,包含因变量范畴)
需要学习两个模型
- 第一个模型是潜变量模型,根据上一时刻数据 $x_{t-1}$ (即 $t$ 时刻的输入特征向量 $\mathbf{x}_t$)与上一时刻的潜变量 $\mathbf{h}_{t-1}$ 计算当前时刻的潜变量 $\mathbf{h}_t$
- 第二个模型是数据自回归模型,根据上一时刻数据 $x_{t-1}$ 与当前时刻潜变量 $h_t$ 计算当前时刻预测输出 $\hat{x}_t$ (即当前时刻的输出特征向量 $\hat{\mathbf{y}}_t$ )
8.1.3 基于马尔科夫假设的自回归模型
基于马尔科夫假设结合单隐藏层MLP,利用预测的数据拟合很远的未来数据会失效
序列数据生成
使用正弦函数和噪声成序列数据,时间步为 1000
1 | %matplotlib inline |
序列数据集生成
假设窗口为 $\tau$ ,我们的目的是用 $\tau$ 个历史数据 $\mathbf{x}_t$ 去拟合当前标签 $\mathbf{y}_t=x_t$ ,因此,特征-标签 集为 $\mathbf{x}_t=\{x_{t-1},x_{t-2},\cdots,x_{t-\tau}\}$
最开始的几个数据没有 $\tau$ 个历史数据:
- 若数据序列足够长,则丢弃前面几项,即从 $\tau+1$ 时刻开始估计
- 填充 $\tau$ 个零
1 | tau = 4 |
时间窗口的第1步取值范围为 $x[0,996]$ ;第2步取值范围 $x[1,997]$ ;第3步取值范围 $x[2,998]$ ;第4步取值范围 $x[3,999]$
feature[0]=[x[0],x[1],x[2],x[3]]
表示第一个样本的四个窗口值;feature[1]=[x[1],x[2],x[3],x[4]]
feature[996]=[x[996],x[997],x[998],x[999]]
即采用丢弃前 $\tau$ 个数据的方法,从 $\tau+1$ 时刻开始估计
使用前600个序列样本进行训练
1 | batch_size, n_train = 16, 600 |
回归模型
使用有两个全连接层的MLP
1 | # 初始化网络权重的函数 |
模型训练
1 | def train(net, train_iter, loss, epochs, lr): |
预测
模型单步预测表现
1 | onestep_preds = net(features) |
单步预测效果不错,600+4作为测试数据,结果也是可信的。
但前提是滑动窗口内的4个数据都是真实的,即
多步预测表现
若真实观测只到 $t$ 时刻,对于 $t+k$ 时刻的预测 $\hat{x}_{t+k}$ 称为 $k$ 步预测,即必须依据自己的预测值作为预测未来的历史数据。如:真实数据记录到 $x_{604}$ ,$\hat{x}_{605}$ 为 $x_{604}$ 的单步预测,$\hat{x}_{606}$ 为 $x_{604}$ 的2步预测,$\hat{x}_{604+k}$ 为 $x_{604}$ 的k步预测
1 | # 真实数据直到x[604] |
结果如下
1 | d2l.plot([time, time[tau:], time[n_train + tau:]], |
605开始为预测数据,刚开始还比较正常,后续趋势就不正常了
因为误差的积累:每次预测未来数据都会有一定误差,预测误差的累积造成 $k$ 步预测的困难
对于单步预测,积累了误差 $\epsilon_1=\overline{\epsilon}$
单步预测的结果被用作2步预测的历史数据,因此2步预测的结果被扰动了 $\epsilon_1$ ,误差累积为 $\epsilon_2=\overline{\epsilon}+c\epsilon_1$
因此,误差的累积会相当快地偏离真实结果
如:天气预报未来24小时相当准确,但超过后精度会迅速下降
进一步观察 $k$ 步预测的困难
1 | max_steps = 64 |
多步预测总结
马尔科夫假设,短期预测可行,长期预测会失效
多步预测,填充的是预测值,而不是真实观测值
虽然 $\tau$ 增大会使模型更准确,但同时也会导致训练样本变少。同时也会导致计算量变大
8.1.4 改进
时间序列窗口只是学习局部的时序特征,给定一个窗口的输入,可以根据输入的时序特征预测出未来的数据。
时序窗口可根据数据调整,即自动选择预测数据所需的相关数据——transformer
8.2 语言模型
语言模型:将整个文本当做时序序列,样本间有时序信息
8.2.1 数据—文本预处理
文本预处理,将文本数据变为可训练的样本
- 将文本作为字符串加载到内存中
- 将字符串拆分为词元
- 建立词表,将词元映射到数字索引
- 将文本转换为数字索引序列
从文件将数据集读入内存
数据集在内存中为一维张量,0轴维度为文本行数
1 | import collections |
将字符串拆分为词元
将文本序列拆分为二维词元张量,0轴维数为文本行数,1轴维度为当前行的字符串所含字符串词元的数量——序列
词元 (token):文本的基本单位,模型最小的可识别数据
1 | def tokenize(lines, token='word'): #@save |
构建词表
文本数据集的所有词元,是一个有限集,将字符串类型的词元映射为从0开始的数字索引,即词表的作用
将训练文本数据集中的所有词元合并在一起,对词元进行唯一统计,得到的统计结果为 语料
根据每个唯一词元的出现频率,为其分配一个数字索引。出现次数越多的词元,索引越小,很少出现的词元通常被移除
对于不在语料库中的词元(及因出现次数过少而删除的词元)映射到特殊的未知词元
<unk>
对于保留字词元,使用单独的序列保存:
<pad>
填充词元;<bos>
序列开始;<eos>
序列结束
1 | class Vocab: #@save |
1 | vocab = Vocab(tokens) |
封装
将练习用到的所有功能打包到 load_corpus_time_machine()
中,返回词表 vocab
和数字索引的文本数据集 corpus
1 | def load_corpus_time_machine(max_tokens=-1): #@save |
8.2.2 语言模型
语言模型是一种时间序列模型,给定序列文本 $x_1,\cdots,x_T$ ,$x_t$ 可被认为文本序列在时间步 $t$ 处的观测或标签。语言模型的目标是估计文本序列出现的联合概率
应用:
- 做预训练模型:BERT,GPT(Generative Pre-Training Transformer)
- 给定前面几个词,生成文本:不断使用 $x_t\sim p(x_t\vert x_{t-1},x_{t-2},\cdots,x_1)$ 来生成后续文本
- 判断多个序列中那个更常见:语音识别到 “to recognize speech” 和 “to wreck a nice beach” ,判断那个文本序列出现的概率高
计数建模
使用计数建模——贝叶斯
假设序列长度为 $2$ ,预测 $p(x,x’)=p(x)p(x’\vert x)=\frac{n(x)}{n}\frac{n(x,x’)}{n(x)}$
- $n$ 为语料数据集的 len ,即数据集中所有词元的个数(含重复词元)
- $n(x)$ 为单个单词出现的次数,$n(x,x’)$ 为连续单词对出现的次数
若序列长度为3,预测 $p(x,x’,x’’)=p(x)p(x’\vert x)p(x’’\vert x,x’)=\frac{n(x)}{n}\frac{n(x,x’)}{n(x)}\frac{n(x,x’,x’’)}{n(x,x’)}$
但使用原始的条件概率模型,可能因文本量不够大,导致 $n(x_1,x_2,\cdots,x_T)\le 1$
一种不可行的方法是拉普拉斯平滑
无效原因:
- 要存储所有的平滑常数 $\epsilon$
- 完全忽略了时序含义
- 只关注历史出现的词元组合,对于过去未出现的序列表现不佳
马尔科夫建模
使用马尔科夫假设可以缓解,设定时间步长,减少序列长度
一元语法:独立的,与历史数据无关
二元语法:仅与前一个时刻的数据有关
三元语法:与前两个时刻的数据有关
假设有1000个词,二元语法,只需要保存两个词的组合概率,共 1000*1000个组合,还需存1000个词单独出现的概率。此时若查任意长度的序列 $\{x_1,x_2,\cdots,x_T\}$ 出现概率 $p(x_1,\ldots,x_T)$ 的时间复杂度为 $O(T)$
1 | import random |
1 | freqs = [freq for token, freq in vocab.token_freqs] |
在消除前几个高频词和后几个低频词后,词频以 parallel log 的方式迅速衰减,意味着单词的频率满足 齐普夫定律(Zipf’s law)
即 $\log n_i=-\alpha \log i+c$
多元语法与词频是否满足双对数曲线
1 | # 二元语法的token,用元组表示 |
1 | # 二元语法的token,用元组表示 |
1 | bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs] |
- 多元语法的单词序列也遵循 齐普夫定律
- $n$ 个单词的 $k$ 元语法理论上会有 $n^k$ 种,但实际上单词之间组合是有特定意义的,某些组合并不会出现,因此拉普拉斯平滑并不适用于语言模型。若去掉低频组合后,会使得 $k$ 元组合的个数比单词的个数 $n$ 还少
- 这种现象说明,语言中存在相当多的时序结构,这个时序结构也是我们后续需要学习的
8.2.3 数据集
文本序列是可以任意长的,所以需要将这个文本序列划分为具有相同长度的子序列
模型一次可以处理时间步为 $T$ 的批量子序列,即时序窗口为 $T$ ,需要生成批量子序列的输入特征 $\mathbf{X}$ 及相应的标签 $\mathbf{Y}$ 用作训练 。一个子序列有 $T$ 个连续的 token
,一个批量子序列有 batch_size
个子序列
对数据集的划分最简单的方法是从随机起始位置取连续的 $T$ 个 token
,代价是某个token会被重复计算,造成浪费
改为将文本序列每隔 $T$ 个时间步划分为一个子序列,每次随机取一个批量的子序列
但若是固定划分子序列,会造成一些如下方红色子序列无法被采样。
为解决这个问题,每个 epoch 都随机选择初始位置,从随机初始位置开始重新划分子序列,理论上可以遍历所有可能的子序列
例如:若规定小批量序列长度为5个时间步,每个时间步的词元为一个字符,则可以选择任意初始位置划分子序列
将文本数据
corpus
分为 $T$ 个token
的若干个子序列(上图中token为一个字符)从 $[0,T-1]$ 中随机选择一个数作为初始位置索引
随机采样:随机子序列组成一个批量
取所有子序列的初始位置下标
构成每个batch需要
batch_size
个子序列, 一个子序列有 $T$ 个token
,一个批量共有 $T\times \text{batch_size}$ 个token
从随机初始位置开始,至 $\text{batch_size}\times T-1$ ,每隔 $T$ 个词元取一个位置索引即可取出子序列初始索引列表
打乱初始索引列表的下标
逐批量返回
[<特征,标签>]
批量多大,则从初始位置list中取几个初始索引
特征:corpus[初始索引:初始索引+时间步长T]
标签:corpus[初始索引+1:初始索引+时间步长T+1]
顺序分区:顺序子序列组成一个批量
- 丢弃开始
offset
个时刻的词元后,将整个corpus
整分为batch_size
折,每次取不同折的一个子序列即可组成一个批量。需要的词元数 $\le\text{num_tokens}$ Xs
为corpus[offset:offset+num_tokens]
,Ys
为corpus[offset+1:offset+num_tokens+1]
- 将
Xs
与Ys
都变形为 $\text{batch_size}\times \frac{\text{num_tokens}}{\text{batch_size}}$ ,可划分的批量数为 $\text{num_batchs}=\frac{\text{num_tokens}}{\text{batch_size}\times T}$ - 每次从
Xs
与Ys
取不同折的一个子序列即可组成一个批量
实现
随机采样 :每个批量都是在原始数据语料集中不重复随机捕获的子序列集
1 | # num_steps为时间窗口 |
1 | my_seq = list(range(35)) |
顺序分区 :保证两个相邻的小批量中,子序列在原始序列上也是相邻的
1 | def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save |
封装返回数据集迭代器
1 | class SeqDataLoader: #@save |
1 | def load_data_time_machine(batch_size, num_steps, #@save |
8.3 RNN
8.3.1 基本概念
隐变量模型
潜变量自回归模型使用潜变量 $\mathbf{h}_t$ 总结历史信息
RNN相较潜变量自回归模型更简单一些
- 隐变量 $\mathbf{h}_t$ 与上一时刻的输入 $x_{t-1}$ 与隐变量 $\mathbf{h}_{t-1}$ 有关
- 当前时刻的输出标签 $o_t$ 仅与当前时刻的隐变量 $\mathbf{h}_t$ 有关
- 隐变量: $\mathbf{h}_t=\phi(\mathbf{W}_{hh}\mathbf{h}_{t-1}+\mathbf{W}_{hx}\mathbf{x}_{t-1}+\mathbf{b}_h)$ :
token
会被表示为 one-hot 向量作为输入 $\mathbf{x}_{t-1}$ - 输出标签: $\hat{\mathbf{y}}_t=\phi(\mathbf{W}_{ho}\mathbf{h}_t+\mathbf{b}_y)$
损失指预测输出 $\hat{\mathbf{y}}_t$ 与真实输出 $\mathbf{y}_t$ 之间的差异。真实输出 $\mathbf{y}_t=x_t$ ,因此隐变量模型是自回归模型
- 预测输出 $\hat{\mathbf{y}}_t$ 时不会用到 $x_t$ ,等价于用历史信息 $\mathbf{x}_{t}=\{x_{t-1},\dots,x_{t-\tau}\}$ 与 $\mathbf{h}_{t-1}$ 预测标签
隐变量模型的理解
RNN 对 $t$ 时刻的输入特征向量 $\mathbf{x}_{t}=x_{t-1}$ 与隐变量 $\mathbf{h}_{t-1}$ 的时序特征进行提取,隐变量 $\mathbf{h}_t$ 中记忆的是对两个信息在时序特征上的组合表示。因此,$\mathbf{h}_t$ 是对历史信息 $\{x_{t-1},\dots,x_{t-\tau}\}$ 中提取到的时序特征的记忆, 其记忆能力由 隐变量的神经元数量(超参数 num_hiddens
)来控制。若设置不合理,容易造成过拟合。
在参数学习过程中,通过误差反向传播调整状态内神经元对输入的响应程度。隐变量 $\mathbf{h}_t$ 对 输入特征向量 $\mathbf{x}_t$ 与 隐变量 $\mathbf{h}_{t-1}$ 的响应程度由参数 $\mathbf{W}_{hh},\mathbf{W}_{hx}$ 决定;输出特征向量 $\mathbf{o}_t$ 对 隐变量 $\mathbf{h}_t$ 的响应程度由参数 $\mathbf{W}_{hq}$ 决定。
RNN的通用近似定理
循环神经网络的通用近似定理:如果一个完全连接的循环神经网络有足够数量的 $sigmod$ 型隐藏神经元,它可以以任意的准确率去近似任何一个非线性动力系统
- 动力系统:系统状态按一定的规律随时间变化的系统——状态间的时序结构
其中,$s_t$ 为每个时刻的隐状态,$g(\cdot)$ 是可测的状态转换函数,$x_t$ 为某时刻的外部输入,$o(\cdot)$ 为连续输出函数,且对状态空间的紧致性没有限制
理论上,循环神经网络可以近似任意的非线性动力系统
前馈神经网络可以近似任何函数
循环神经网络可以模拟任何程序
应用
8.3.2 RNN模型
(词元)状态的表示
$\mathcal{V}$ 为所有(词元)状态 token
的词表 vocab
,若有 $V$ 个 token
,则每个 token
需要长度 $V$ 的 one-hot 向量表示
每个(词元)状态都对应 $\mathcal{V}$ 中的一个数字索引,但直接使用数字索引进行反向传播,会导致损失计算困难,所以需要进一步转换为 one-hot 向量。
one-hot 向量:将每个索引映射为互不相同的单位向量
- 如果(词元)状态的索引为 $\nu$ ,则创建长度为 $V$ 的全0向量,再将第 $\nu$ 个元素设置为 $1$
训练数据集
实际应用中,数据是逐批量被传入模型的
令 $T\triangleq时间步长,B\triangleq批量大小=\vert \mathcal{B}\vert ,V\triangleq词表大小=\vert \mathcal{V}\vert$ ,$h\triangleq 隐变量神经元数量$
$\mathbf{X}\in \mathbb{R}^{T\times B\times V}$ 为一个批量的子序列在一个时间窗口的状态
- $[\mathbf{X}]_{t}\in \mathbb{R}^{B\times V},t\in [1,T]$ 表示在 $t$ 时刻一个批量的子序列的(词元)状态
- $\mathbf{x}_{t,i}\in \mathbb{R}^{1\times V}$ 表示在第 $i$ 个子序列中,$t$ 时刻的(词元)状态
$\mathbf{H}\in \mathbb{R}^{T\times B\times h}$ 为一个批量的子序列在一个时间窗口的隐变量
$[\mathbf{H}]_t\in \mathbb{R}^{B\times h},t\in [1,T]$ 表示一个批量子序列的(词元)状态在 $t$ 时刻的隐变量
$\mathbf{h}_{t,i}\in\R^{1\times h}$ :从 $\mathbf{h}_{t-1,i}$ 和 $\mathbf{x}_{t-1,i}$ 提取的时序特征
参数:
$\mathbf{W}_{hh}\in \mathbb{R}^{h\times h}$ ,$\mathbf{W}_{hx}\in \mathbb{R}^{h\times V}$ :保存状态间的时序信息
$\mathbf{W}_{qh}\in \mathbb{R}^{q\times h}$ :保存隐状态与输出的关系
- $\mathbf{b}_h\in\R^{1\times h},\mathbf{b}_q\in\R^{1\times q}$
- 即使在不同的时间步,循环神经网络也总是使用这些模型参数。 因此,循环神经网络的参数开销不会随着时间步的增加而增加
正向传播
关于 $\mathbf{x}_{t,i}$ 与 $x_{t-1,i}$ :仅用 $\mathbf{x}_{t,i}$ 表示一个批量中的第 $i$ 个子序列在 $t$ 时刻的输入特征向量,其值等于上一时刻的真实值 $x_{t-1,i}$ 。对于自回归模型,第 $t$ 时刻的预测输出要去拟合 $t$ 时刻的真实值,即 $\hat{\mathbf{y}}_{t,i}=f(\mathbf{x}_{t,i})\rightarrow x_{t,i}$
按批量计算时,正向传播
相当于
在真正的可运行代码中,执行的是矩阵乘法,而不是对一个批量子序列的 token
计算 $T\times B$ 次
一次正向传播,为T次矩阵乘法,一次矩阵乘法相当于整批量、整序列前进一个时间步
当前时间步的输出为
一个批量在 $t$ 时刻的预测输出为
隐变量 $\mathbf{h}_{t,i}$ 记录一个子序列在当前时间步对历史信息的总结,相当于神经网络当前的状态,因此也被称为隐状态
由于当前时间步,隐状态使用的定义与前一个时间步中使用的定义相同,所以在一个时间窗口 $T$ 内,隐状态的更新是循环的。因此,隐状态更新是循环计算的层 $\mathbf{H}\in \mathbb{R}^{T\times B\times V}$ 称为 循环层
在真正的GPU计算中,$\mathbf{X}_t \mathbf{W}_{xh} + \mathbf{H}_{t-1} \mathbf{W}_{hh}$ 的计算,相当于 $\mathbf{X}_t$和$\mathbf{H}_{t-1}$ 的拼接 与 $\mathbf{W}_{xh}$ 和 $\mathbf{W}_{hh}$ 的拼接的矩阵乘法
- 大矩阵计算的多线程优化会更好,同样的计算量,多次小矩阵乘法的开销大于一次大矩阵乘法的开销。Python有开销,GPU核也有开销
预测输出
若 one-hot 向量 $\mathbf{y}_{t,i}=x_{t,i}$ 的第 $\nu_i$ 个为1,即 $t$ 时刻的 真实输出 在词表中的数字索引为 $\nu_i$
时序任务可以理解为一个分类任务,每个时刻的输出 $\hat{\mathbf{y}}_{t,i}$ 相当于 $V$ 分类问题
- 可视化输出,即 $\text{vocab.idx_to_token}\left(\arg \max\limits_{v} softmax\left(\mathbf{o}_{t,i}\right)^{(v)}\right),v\in [0,V-1]$
损失函数
预测正确即分为正确类的概率
对于第 $i$ 个子序列在 $t$ 时刻预测输出的交叉熵损失为
时间序列模型的一个样本 $\mathbf{x}_i$ 为时间步长为 $T$ 的子序列,一个样本需要进行 $T$ 次标签的预测输出,即 $T$ 次V分类,所以整个时序模型的好坏用平均交叉熵衡量
在语言模型中,将指数化平均交叉熵作为衡量指标
放大平均交叉熵
1表示完美,预测输出确信是一个值,对应 $p\left(x_{t,i}\vert x_{t-1,i},\ldots,\right)=1$
若 $\exp(\pi_i)>1$ ,对应 $\sum-\log p\left(x_{t,i}\vert x_{t-1,i},\ldots,\right)>0\Rightarrow \sum\log p\left(x_{t,i}\vert x_{t-1,i},\ldots,\right)<0$ ,即有多次预测 $0<p\left(x_{t,i}\vert x_{t-1,i},\ldots,\right),p\left(x_{t’,i}\vert x_{t’-1,i},\ldots,\right)<1$
一个批量 $\vert \mathcal{B}\vert$ 个子序列的损失函数为
参数学习
使用梯度下降法进行参数更新时
因此,关键是要计算损失函数对参数的梯度
输出层参数学习
对于 $\frac{\partial \ell\left(\mathbf{o}_{t,i},\mathbf{y}_{t,i}\right)}{\partial \mathbf{W}_{qh}}$ ,使用链式法则
循环层参数学习
对于 $\frac{\partial \ell\left(\mathbf{o}_{t,i},\mathbf{y}_{t,i}\right)}{\partial \mathbf{W}_h}$ ,使用链式法则
计算 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_h}$
对于 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_{hx}}\in \mathbb{R}^{h\times h\times V}$ ,由于 $\mathbf{h}_{t-1,i}$ 与 $\mathbf{x}_{t,i}\mathbf{W}_{hx}^{\mathsf{T}}$ 都是关于 $\mathbf{W}_{hx}$ 的复合函数,所以
对于 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_{hh}}$ ,由于 $\mathbf{h}_{t,i}$ 的正向传播不仅依赖于 $\mathbf{h}_{t-1,i}\mathbf{W}_{hh}^{\mathsf{T}}$ ,而且循环依赖于 $\mathbf{h}_{t-1,i}$ ,所以
因此,可将 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_{hx}}$ 与 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_{hh}}$ 统一表示为 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_h}=\frac{\partial f(t)}{\partial \mathbf{W}_{h}}+\frac{\partial f(t)}{\partial \mathbf{h}_{t-1,i}}\frac{\partial \mathbf{h}_{t-1,i}}{\partial \mathbf{W}_{h}}$
- 若序列 $\{a_t\},\{b_t\},\{c_t\}$ 满足 $a_t=b_t+c_ta_{t-1}$ ,则有
虽然每个时间步长的 $\mathbf{W}_h$ 都相同,但当 $t$ 与 $u$ 差距很大, $\mathbf{W}_{hh}^{t-u}$ 这个连乘链就会变得很长,造成数值不稳定问题
时间步截断
在 $\tau$ 步后截断,使得梯度求和终止于 $\frac{\partial f(t-\tau)}{\partial \mathbf{W}_h}$ ,即变为 $\mathbf{W}_{hh}^{t-\tau}$ 的连乘
在后续实现中对每个序列梯度的 detach()
就是实现这个方法。
实践中,效果好,虽然会导致该模型主要侧重于短期影响,但会将估计值偏向更简单和稳定的模型
随机截断
使用一个随机变量替换 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_h}$ ,这个随机变量为序列 $\{\xi_t\}$ ,预定义 $\alpha\in [0,1]$ ,且 $p(\xi_t=0)=1-\alpha_t$ 且 $p\left(\xi_t=\alpha_t^{(-1)}\right)=\alpha_t$ ,因此 $E[\xi_t]=1$
则梯度 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_h}$ 被代替为
有 $E[z_t]=\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_h}$ ,每当 $\xi=0$ 时,循环层梯度终止于 $t$ 这个时间步。随机截断的好处在于长序列很少出现。
实际中,时间步截断比随机截断效果好:
- 在对过去若干个时间步经过反向传播后, 观测结果足以捕获实际的依赖关系
- 增加的方差抵消了时间步数越多梯度越精确的事实
- 我们真正想要的是只有短范围交互的模型
计算 $\frac{\partial \ell}{\partial \mathbf{h}_{t,i}}$
对于最后一个时间步 的输出 $\mathbf{o}_{T,i}$ ,依赖于相应的隐变量 $\mathbf{h}_{T,i}$
则
对于任意时间步 $t<T$,由于损失函数 $\ell$ 通过 $\mathbf{h}_{t+1,i}$ 和 $\mathbf{o}_{t,i}$ 依赖于 $\mathbf{h}_{t,i}$ 由链式法则 $\frac{\partial \ell}{\partial \mathbf{h}_{t,i}}\in \mathbb{R}^{1\times h}$
对于任意时间步 $1\le t\le T$ 展开递归式
合并
连乘会使得 $\mathbf{W}_{hh}$ 存在很大的幂,小于1的值会消失,大于1的值将发散。这种数值不稳定问题表现为梯度消失或梯度爆炸。
当对 $\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_h}$ 采用0步截断后,
误差反向传播
通过时间反向传播BPTT实际上是误差反向传播在RNN的一个特定应用
将RNN的计算图一次展开一个时间步,以获得模型变量和参数之间的依赖关系。基于链式法则,应用反向传播来计算和存储梯度。
由于一个子序列可能相当长,因此参数间的依赖关系也相当长。时序窗口为1000的子序列,其第一个词元可能会对最后位置的词元产生很大影响,且还需要超过1000个矩阵的乘积才能得到梯度 $\mathbf{g}$
数值稳定性解决
在长为 $T$ 的时间窗口上,迭代计算 $T$ 个时间步上的梯度,在反向传播中会产生长度为 $O(T)$ 的矩阵乘法链。当 $T$ 较大,会造成数值不稳定问题。
一般来说,使用梯度下降法从负梯度方向更新参数 $\mathbf{W}$ ,即 $\mathbf{W}\leftarrow \mathbf{W}-\eta \mathbf{g}$ 。若进一步假设函数 $f(x)=x-\eta g$ 有比较好的性质,在常数 $L$ 下是利普希茨连续的,对于任意的
则通过 $\eta \mathbf{g}$ 更新参数向量时
即我们不会观察到梯度有超过 $L\eta\Vert \mathbf{g}\Vert$ 的变化。好的方面是:当优化方向错误是,变坏的程度小;坏的方面是限制了优化速度
若梯度很大时,优化算法可能无法收敛,可以通过降低学习率 $\eta$ 来解决。但大梯度只是小概率时间,则直接降低学习率会使训练速度变得很慢,也是不可取的。
一种替代方案是将 梯度裁剪,只有梯度的模超过一个阈值才进行投影,也能达到防止梯度爆炸,使目标函数收敛的目的。
梯度裁剪的方法是限制梯度连乘的模 $\Vert \mathbf{g}\Vert$ 不超过 $\theta$
- 若计算一个子序列误差反向传播时,产生模为 $\Vert \mathbf{g}\Vert$ 的梯度 $\mathbf{g}$ ,若这些梯度的模大于 $\theta$ ,则导致 $\frac{\theta}{\Vert \mathbf{g}\Vert}>1$ ,从而使得 $\min\left(1,\frac{\theta}{\Vert\mathbf{g}\Vert}\right)\mathbf{g}=\frac{\theta}{\Vert\mathbf{g}\Vert}\mathbf{g}$ ,即使裁剪后的梯度模为 $\theta$
误差正向传播
Real-Time Recurrent Learning,RTRL:通过前向传播的方式来计算梯度
假设RNN第 $i$ 个子序列在 $t$ 时刻的预测输出状态为 $\mathbf{o}_{t,i}$
故有:
$\mathbf{h}_{t+1,i}$ 通过 $\mathbf{h}_{t,i}\mathbf{W}_{hh}^{\mathsf{T}}$ 与 $\mathbf{h}_{t,i}$ 依赖于 $\mathbf{W}_{hh}$,故
实时循环学习算法从第一个时刻开始,除了计算隐状态外,还需要依次向前计算偏导 $\frac{\partial \mathbf{h}_{t,1}}{\partial \mathbf{W}_{hh}},\frac{\partial \mathbf{h}_{t,2}}{\partial \mathbf{W}_{hh}},\cdots,\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_{hh}}$
当 $t+1=T$ 时,损失函数关于参数的偏导可得
$\mathbf{h}_{t+1,i}$ 通过 $\mathbf{x}_{t,i}\mathbf{W}_{hx}^{\mathsf{T}}$ 与 $\mathbf{x}_{t,i}$ 依赖于 $\mathbf{W}_{hx}$,故
实时循环学习算法从第一个时刻开始,除了计算隐状态外,还需要依次向前计算偏导 $\frac{\partial \mathbf{h}_{t,1}}{\partial \mathbf{W}_{hx}},\frac{\partial \mathbf{h}_{t,2}}{\partial \mathbf{W}_{hx}},\cdots,\frac{\partial \mathbf{h}_{t,i}}{\partial \mathbf{W}_{hx}}$
当 $t+1=T$ 时,损失函数关于参数的偏导可得
BTPP&RTRL
二者都是基于梯度下降的算法,分别通过前向模式和反向模式应用链式法则计算梯度
循环神经网络中,一般网络输出维度低于输入维度,BTPP的计算量会更小,但BPTT需要保存所有时刻的中间梯度,空间复杂度较高
RTRL算法不需要梯度回传,因此非常适合用于在线学习和无限序列的任务中
RNN的长程依赖问题
当时序窗口 $T$ 很大时, 对于较早期的 $t$
若 $[\mathbf{W}_{hh}]_{ij}>1$ ,当 $T-u\rightarrow \infty$ 时,$\delta_{T,u}\rightarrow \infty$
- 当 $T-u$ 间隔比较大时,梯度也变得很大,造成系统不稳定,称为梯度爆炸问题
若 $[\mathbf{W}_{hh}]_{ij}<1$ ,当 $T-u\rightarrow \infty$ 时,$\delta_{T,u}\rightarrow 0$
当 $T-u$ 间隔比较大时,梯度将变得很小,出现梯度消失问题
在循环神经网络中,梯度消失不是指 $\frac{\partial \ell\left(\mathbf{o}_{t,i},\mathbf{y}_{t,i}\right)}{\partial \mathbf{W}_h}$ 消失,而是 $\delta_{T,u}\rightarrow 0$
$\frac{\partial \ell\left(\mathbf{o}_{t,i},\mathbf{y}_{t,i}\right)}{\partial \mathbf{W}_h}$ 是求和得到的,为0的项不影响
即 参数 $\mathbf{W}_{h}$ 的更新主要靠时刻 $T$ 和$u$个相邻状态来更新,长距离的状态对参数 $\mathbf{W}_{h}$ 是没有影响的
虽然简单循环网络理论上可以建立长时间间隔的状态之间的依赖关系,但由于梯度爆炸或梯度消失问题,实际上只能学习到短期的依赖关系
简单的神经网络很难建模长距离的依赖关系,称为 长程依赖问题
8.3.3 实现
导入数据
1 | import math |
一个epoch会完整扫一次数据集
train_iter
在每次被遍历时,都会调用批量生成函数,即每次迭代都是重新生成的训练数据
1 | for X,Y in train_iter: |
从迭代起中拿到的 $X\in \mathbb{R}^{B\times T}$ ,$X_{i,j}\in \mathbb{R}$ 为 vocab
中的数字索引。
在转换为 one-hot 向量之前,需要将其转置,变为 $X^{\mathsf{T}}\in \mathbb{R}^{T\times B}$ 。一次矩阵乘法,可以让整个批量下的所有(词元)状态都前进一个时间步,确保两步之间的状态是连续的。
这个过程可以理解为
1 | # 从迭代器中拿到的是(批量大小,时间步长) (2,5) |
初始化模型参数
1 | def get_params(vocab_size, num_hiddens, device): |
模型定义
序列的初始隐状态初始化
对于每个子序列的初始隐状态 $\mathbf{h}_{0,i}\in \mathbb{R}^{1\times h}$ ,不能通过状态循环更新公式迭代,所以必须直接初始化
由于一个时间步长的状态是按批量更新的,且一个批量有 $B$ 个子序列,所以批量的初始隐状态为 $\mathbf{H}_{0}\in \mathbb{R}^{B\times h}$
1 | def init_rnn_state(batch_size, num_hiddens, device): |
正向传播
整个批量子序列逐时间步更新隐状态 $\mathbf{H}\in \mathbb{R}^{T\times B\times V}$ ,此处非线性函数使用 $\tanh$
1 | def rnn(inputs, state, params): |
批量不同,意味着输入 $\mathbf{X}$ 不同,相应隐变量 $\mathbf{H}$ 也不同,一个批量正向传播过程中,隐变量 $\mathbf{H}$ 会更新 $T$ 次,而最后一个时刻输入 $\mathbf{x}_{T,i}$ 的隐变量 $\mathbf{h}_{T,i}$ 也会被输出,当两个批量的子序列连续时,可以用其作为后一个批量的起始隐状态
RNN模型封装
1 | class RNNModelScratch: #@save |
1 | num_hiddens = 512 |
5个时间步长组成一个子序列,有2个子序列组成一个批量,隐状态神经元数量为512,则期望的输出为 $(T\times B,V)=(5\times 2,28)$ ,预测输出对应的隐状态 $H_{T}\in \mathbb{R}^{B\times V}$
预测
prefix
为待预测的输入,需要使用 prefix
生成代表整个序列时序特征的隐状态 $\mathbf{h}_{T}$ ,这个阶段称为预热
预热完成获取 prefix
的时序特征,才能对 prefix
进行预测
1 | # 功能:遍历prefix做预热,将prefix的历史信息保存在隐状态state中, |
梯度裁剪
1 | def grad_clipping(net, theta): #@save |
训练
一个epoch
序列数据的不同采样方式会导致隐状态初始化的区别
当使用顺序分区,即下一个批量数据的第 $i$ 个子序列与当前批量的第 $i$ 个子序列相邻,因此当前批量的最后一个时刻的隐状态将用于初始化下一个批量子序列第一个输入
token
的隐状态。同时为了不增加梯度计算的复杂度,在处理每个批量前,先分离梯度,使隐状态的梯度计算总是在一个批量的时间窗口内
当使用随机采样时,每个子序列都是随机位置抽取的,所以需要为每个子序列初始化隐状态
更新模型参数前,需要裁剪梯度,即使训练过程中某个点发生梯度爆炸,模型也不会发散
用困惑度来评价模型,确保不同长度的序列有可比性
1 | #@save |
多轮epoch
1 | #@save |
1 | num_epochs, lr = 500, 1 |
随机采样的结果
1 | # 子序列随机采样组成批量, |
8.3.4 简洁实现
导入数据
1 | import torch |
模型定义
定义一个隐状态层
1 | num_hiddens = 256 |
- 输入是 $1\times V$
- 参数 $\mathbf{W}_{hx}\in \mathbb{R}^{h\times V},(256\times 28)$
初始化隐状态,形状是 $(隐状态层数,批量大小,隐状态神经元数)$
1 | state = torch.zeros((1, batch_size, num_hiddens)) |
通过一个隐状态和一个输入,就可以更新隐状态
1
2
3
4
5
6X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape
# Y:(时间步长*批量大小,隐藏层神经元数量)
# 并没有做cat,若cat,则Y形状为(时间步长*批量大小,隐藏层神经元数量)
RNN模型定义
与原实现不同的在于,循环层 nn.RNN()
的输出 output
只是 循环层的隐状态张量 $\mathbf{H}_T$
还需要一个 nn.linear
全连接层作为预测输出层 nn.Linear(self.num_hiddens, self.vocab_size)
,将时序特征 output
转换为输出 $\mathbf{O}_T$
rnn_layer
是一个RNN模型的单个循环层,输入批量子序列和初始隐状态,对时序窗口内的每个词元遍历循环操作,返回输出 $\mathbf{O}$ 和累积了整个子序列时序特征的隐状态 $\mathbf{H}$
1 | #@save |
训练与预测
1 | device = d2l.try_gpu() |
预训练骨干网络:科研团队,YOLO等框架,
模型应用:个人,YOLO结合transformer解决图像分割、目标检测、语义分割等问题