参考
循环神经网络
一文搞懂RNN(循环神经网络)基础篇
零基础入门深度学习(5) - 循环神经网络
史上最详细循环神经网络讲解(RNN/LSTM/GRU)

循环神经网络(recurrent neural network,RNN)

到目前为止默认数据都来自于某种分布,并且所有样本都是独立同分布的.然而大多数数据并非如此。
不仅仅可以接收一个序列作为输入,而是还可能期望继续猜测这个序列的后续。

循环神经网络(recurrent neural network,RNN)则可以更好地处理序列信息。 循环神经网络通过引入状态变量存储过去的信息和当前的输入,从而可以确定当前的输出。

自回归模型

维度 自回归模型(AR) 隐变量自回归模型(Latent AR)
是否含隐变量 是,隐变量为概率意义上的状态
数据依赖 仅自身历史数据 依赖历史观测 + 推断出的隐状态
趋势建模能力 线性、平稳趋势 可建模非线性、状态切换、多模态序列
推断方式 参数估计(如最小二乘) 概率推断(如EM、变分推断)
表达能力 有限,主要适用于简单结构序列 表达力强,适用于复杂动态系统

自回归模型(autoregressive models,AR)

使用时间序列自身的过去值的线性组合来预测未来值
$$
X_t = c + \sum_{i=1}^p \phi_i X_{t-i} + \epsilon_t
$$

  • $p$:滞后阶数;
  • $\phi_i$:模型参数;
  • $\epsilon_t$:白噪声项。

缺陷:仅依赖自身历史观测值;假设数据平稳,趋势是线性的;无需建模潜在状态;适用于简单趋势建模,不擅长处理状态切换或复杂行为。

隐变量自回归模型(latent autoregressive models)

引入不可观测的隐状态变量(latent variable),作为对历史信息的压缩总结,通过该状态驱动观测值的生成。

$$
\begin{cases}
z_t = f(z_{t-1}, x_{t-1}) & \text{(隐状态递归更新)} \\
x_t \sim p(x_t \mid z_t) & \text{(观测值生成)}
\end{cases}
$$

  • $z_t$:隐状态(通常为概率分布);
  • $f$:状态转移函数(可为神经网络或状态空间模型);
  • $p(x_t \mid z_t)$:条件生成分布。

特点:能建模状态转换、周期性、非线性结构;适用于多模态、复杂序列;需要通过统计方法(如 EM 算法、变分推断)估计隐变量的分布;隐状态是软分配的,不是硬选择。

马尔可夫模型

时间序列的当前状态 $X_t$ 理论上依赖于全部过去:

$$
P(X_t \mid X_{t-1}, X_{t-2}, \ldots)
$$

马尔可夫条件假设此依赖可简化为有限阶:

$$
P(X_t \mid X_{t-1}, \ldots, X_{t-p})
$$

当 $p=1$ 时,即为一阶马尔可夫性质,当前状态只依赖前一状态:

$$
P(X_t \mid X_{t-1}, X_{t-2}, \ldots) = P(X_t \mid X_{t-1})
$$

特点对比 马尔可夫模型 自回归模型
状态类型 离散(如晴天、雨天、雪天) 连续数值(如体重)
依赖对象 前一时刻的状态 前几个时刻的观测值
预测方式 状态转移概率 线性组合(加权求和)加白噪声

序列模型

$$
x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1)
$$

$\sim$ 代表“从……中采样”。随机变量 $x_t$ 的取值是根据条件概率分布 $P(x_t\mid x_{t-1}, \ldots, x_1)$ 生成的。当前时刻的 $x_t$ 是依赖于之前所有时刻的变量 $x_1, x_2, \ldots, x_{t-1}$ 的。这反映了序列中元素之间的关联性,说明 $x_t$ 与之前的元素有关联,而非独立产生。

ps:下标顺序只是展示的顺序问题,数学意义上并不影响条件集合的内容。

给定一个长度为 $T$ 的序列 $(x_1, x_2, \ldots, x_T)$,它们的联合概率分布满足链式法则

$$
P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_1, x_2, \ldots, x_{t-1})
$$

时间步(time step)用 $t \in \mathbb{Z}^+$ 表示序列中的第 $t$ 个时间点。
目标是建模序列中任一时刻的输出依赖于之前的若干时间步输入。

自回归模型

第一种策略,假设在现实情况下相当长的序列$x_{t-1}, \dots, x_1$ 可能是不必要的, 因此我们只需要满足某个长度为 $\tau$ 的时间跨度,即使用观测序列 $x_{t-1}, \dots, x_{t-\tau}$。当下获得的最直接的好处就是参数的数量总是不变的,至少在 $t > \tau$ 时如此,这就使我们能够训练一个上面提及的深度网络。这种模型被称为 自回归模型(autoregressive models),因为它们是对自己执行回归。

第二种策略,如图 8.1.2 所示,是保留一些对过去观测的总结 $h_t$,并且同时更新预测 $\hat{x}t$ 和总结 $h_t$。这就产生了基于 $\hat{x}t = P(x_t \mid h_t)$ 估计 $x_t$,以及公式 $h_t = g(h{t-1}, x{t-1})$ 更新的模型。由于 $h_t$ 从未被观测到,这类模型也被称为隐变量自回归模型(latent autoregressive models)。

线性自回归模型(AR(p))

$$
x_t = \phi_1 x_{t-1} + \phi_2 x_{t-2} + \dots + \phi_p x_{t-p} + \epsilon_t
$$

  • $x_t$:当前值
  • $\phi_i$:模型参数
  • $\epsilon_t$:噪声项(白噪声)
  • $p$:窗口长度

非线性自回归模型(如 RNN)

将上式替换为非线性表达:

$$
x_t = f(x_{t-1}, x_{t-2}, \dots, x_{t-p}) + \epsilon_t
$$

或使用隐藏状态方式:

$$
\begin{aligned}
h_t &= f(h_{t-1}, x_{t-1}) \\
\hat{x}_t &= g(h_t)
\end{aligned}
$$

马尔可夫模型(Markov Model)

马尔可夫模型是一个统计建模方法。

时间序列的当前状态 $X_t$ 理论上依赖于全部过去:

$$
P(X_t \mid X_{t-1}, X_{t-2}, \ldots)
$$

马尔可夫条件假设此依赖可简化为有限阶:

$$
P(X_t \mid X_{t-1}, \ldots, X_{t-p})
$$

当 $p=1$ 时,即为一阶马尔可夫性质,当前状态只依赖前一状态:

$$
P(X_t \mid X_{t-1}, X_{t-2}, \ldots) = P(X_t \mid X_{t-1})
$$

特点对比 马尔可夫模型 自回归模型
状态类型 离散(如晴天、雨天、雪天) 连续数值(如体重)
依赖对象 前一时刻的状态 前几个时刻的观测值
预测方式 状态转移概率 线性组合(加权求和)加白噪声

有隐状态的循环神经网络

代码中的 H 被初始化为一个随机值,表示初始时刻的隐藏状态。这一点是非常重要的,因为在序列数据的处理过程中,我们通常需要为网络提供一个初始的隐藏状态,这个初始隐藏状态的选择可以影响模型的学习过程。

H 的初始化表示的是 初始时刻的隐藏状态,它在第一次前向传播时被赋值为随机值,之后的每个时间步中,隐藏状态会根据前一个时间步的隐藏状态和当前的输入来更新。

1
2
3
X, W_xh = torch.normal(0, 1, (3, 1)), torch.normal(0, 1, (1, 4))
H, W_hh = torch.normal(0, 1, (3, 4)), torch.normal(0, 1, (4, 4))
torch.matmul(X, W_xh) + torch.matmul(H, W_hh)

拼接的目的:合并计算,减少操作次数.只是计算上的重排和合并,模型逻辑没变,还是用两个不同的权重矩阵分别对不同输入部分作用,只是计算时合成一步。

1
torch.matmul(torch.cat((X, H), 1), torch.cat((W_xh, W_hh), 0))

循环神经网络的从零开始实现

独热编码

PyTorch 的 F.one_hot 函数将数字序列 [0, 2] 转换为 独热编码(one-hot encoding),是高维稀疏向量。
ps:word embedding可以替代one hot

1
F.one_hot(torch.tensor([0, 2]), len(vocab))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# inputs的形状:(时间步数量,批量大小,词表大小)
def rnn(inputs, state, params):

# 拆包出五个参数,params 是一个元组或列表,里面按顺序包含这些权重和偏置
W_xh, W_hh, b_h, W_hq, b_q = params
# state 是只有一个元素的元组,把元组中第一个元素赋值给变量 H。且RNN 变体可能会返回多个状态这种写法安全而通用
H, = state
# 初始化一个空列表,用于收集每个时间步的输出
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:
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)
return torch.cat(outputs, dim=0), (H,)

循环神经网络的简洁实现

256个隐藏单元,一维张量,长度256。

1
2
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class RNNModel(nn.Module):
"""循环神经网络模型"""
# 定义继承自 nn.Module 的模型类,**kwargs 用于兼容父类 __init__ 的参数
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))

RNN

RNN 本质上就是一个非线性的、自适应的、神经网络形式的自回归模型。
只能从前往后建模:

  • 状态更新:

$$
S_t = f(U V_t + W S_{t-1} + b)
$$

  • 输出计算:

$$
O_t = g(S_t, V_t)
$$

权重在所有时间步(t=1 到 t=T)之间是相同的,同一组权重参数会被用于所有时间步的计算

双向 RNN

同时考虑过去和未来:

$$
\begin{aligned}
h_t^{\rightarrow} &= f(h_{t-1}^{\rightarrow}, x_t) \\
h_t^{\leftarrow} &= f(h_{t+1}^{\leftarrow}, x_t) \\
h_t &= [h_t^{\rightarrow}; h_t^{\leftarrow}] \\
y_t &= g(h_t)
\end{aligned}
$$

向量化

神经网络的输入和输出都是向量,为了让语言模型能够被神经网络处理,我们必须把词表达为向量的形式,这样神经网络才能处理它。

深度循环神经网络

前面循环神经网络只有一个隐藏层,我们当然也可以堆叠两个以上的隐藏层,这样就得到了深度循环神经网络。

循环神经网络的训练算法:BPTT

BPTT算法是针对循环层的训练算法,它的基本原理和BP算法是一样的,也包含同样的三个步骤:

前向计算每个神经元的输出值;
反向计算每个神经元的误差项值,它是误差函数E对神经元j的加权输入的偏导数;
计算每个权重的梯度。
最后再用随机梯度下降算法更新权重。

1
2
```
```python
1
2
```
```python