1021 字
5 分钟
从零开始深度学习 Day 4 Modern RNN

单层 RNN 的改进#

截断时间步#

通过时间反向传播 BUTT 所遇到的问题:

由于隐状态 hth_t 是通过马尔可夫链动态规划生成的,如果要把过程的参数全部求导一遍,计算量是非常恐怖的,而且这样过长的链条会导致:

  • 前几层的轻微扰动对最终结果的影响非常大;
  • 梯度爆炸;

解决方案:我们只计算前几层的偏导数(截断时间步)

LSTM#

Long Short-Term Memory, LSTM

模型结构#

模型结构

相对于 RNN 的传统结构,维护了长短两条线条以及三个门:

  • 遗忘门:算出要遗忘多少长期记忆,,从 STM, Input 获得参数
  • 输入门:其实感觉应该叫长期记忆更新门,从 STM, Input 获得参数
  • 输出门:更新短期记忆,从 STM, Input 获得参数

前向传播#

输入门:

it=σ(Wxixt+Whiht1+bi)i_t = \sigma(W_{xi}x_t + W_{hi}h_{t-1} + b_i)

遗忘门:

ft=σ(Wxfxt+Whfht1+bf)f_t = \sigma(W_{xf}x_t + W_{hf}h_{t-1} + b_f)

细胞状态:

ct=ftct1+gtitc_t = f_t \odot c_{t-1} + g_t \odot i_t

\odot 即逐元素相乘。

中间状态:

mt=tanh(ct)m_t = \tanh(c_t)

隐藏状态更新方程:

ht=otmt=ottanh(ct)\begin{align*} h_t &= o_t \odot m_t \\ &= o_t \odot \tanh(c_t) \end{align*}

输出层:

yt=Wyhht+byy_t = W_{yh}h_t + b_y

计算图#

计算图

深层循环神经网络#

获得更多非线性性

深层

双向循环神经网络#

填空,有点 bert 的感觉。

双向马尔可夫链。

体现了现代深度网络的设计原则: 首先使用经典统计模型的函数依赖类型,然后将其参数化为通用形式。

缺乏物理意义,仅在数学上可行,最终得到的是困惑度很低但是完全错误的文本。

编解码器 序列到序列学习(seq2seq)#

传统模型无法有效处理 “输入和输出都为可变长序列”的任务

用“编码器”负责理解输入序列, 用“解码器”负责生成输出序列。

输入序列 x₁, x₂, ..., x_T
Encoder ——→ 上下文向量 c ——→ Decoder
输出序列 y₁, y₂, ..., y_T'

Encoder#

使用循环神经网络(RNN / LSTM / GRU) 在每个时间步更新隐状态:

ht=f(ht1,xt) h_t = f(h_{t-1}, x_t)

上下文变量通常选为最后时间步的隐状态:

c=hT c = h_T

若使用双向 RNN,则每个隐状态编码了前后文信息

class Seq2SeqEncoder(d2l.Encoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout)
def forward(self, X):
X = self.embedding(X).permute(1, 0, 2)
output, state = self.rnn(X)
return output, state

解码器#

  • 逐步生成输出序列(如翻译的法语句子)。

  • 每个时间步的输出依赖于:

    • 上一个时间步的输出(或真实标签,训练时使用)
    • 编码器输出的上下文变量
    • 自身的隐状态
st=f(st1,yt1,c)s_t = f(s_{t-1}, y_{t-1}, c)P(yty<t,X)=softmax(Wst)P(y_t | y_{<t}, X) = \text{softmax}(W s_t)
class Seq2SeqDecoder(d2l.Decoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs):
return enc_outputs[1]
def forward(self, X, state):
X = self.embedding(X).permute(1, 0, 2)
context = state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), 2)
output, state = self.rnn(X_and_context, state)
output = self.dense(output).permute(1, 0, 2)
return output, state

强制教学#

训练时,将真实的前一时间步输出 ( y_{t-1} ) 作为输入,而不是模型预测值。 这样训练更稳定、收敛更快。

损失函数(带掩码的交叉熵)#

用于忽略填充(<pad>)部分的无效计算:

loss = MaskedSoftmaxCELoss()

利用 sequence_mask() 屏蔽超出有效长度的部分。

seq2seq#

bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0]).reshape(-1, 1)
dec_input = torch.cat([bos, Y[:, :-1]], 1)
Y_hat, _ = net(X, dec_input, X_valid_len)
l = loss(Y_hat, Y, Y_valid_len)
l.sum().backward()
optimizer.step()

束搜索#

大模型优化的贪心,在穷加和贪心,计算成本和准确性间选择平衡。

输入: 模型 f, 输入序列 X, 束宽 k
输出: 最优序列 Y*
初始化:
beams = [("", 0)] # 存储 (序列, 对数概率)
循环直到所有序列结束:
new_beams = []
对于每个 (seq, score) 在 beams:
下一个词的概率分布 P = f(seq | X)
对于每个候选词 y:
new_seq = seq + [y]
new_score = score + log P(y | seq)
new_beams.append((new_seq, new_score))
对 new_beams 按 new_score 排序
保留前 k 个
beams = top_k(new_beams)
返回 beams 中得分最高的序列
从零开始深度学习 Day 4 Modern RNN
https://blog.candlest.cc/posts/ai/rnn_ii/
作者
candlest
发布于
2025-10-22
许可协议
CC BY-NC-SA 4.0