1021 字
5 分钟
从零开始深度学习 Day 4 Modern RNN
单层 RNN 的改进
截断时间步
通过时间反向传播 BUTT 所遇到的问题:
由于隐状态 是通过马尔可夫链动态规划生成的,如果要把过程的参数全部求导一遍,计算量是非常恐怖的,而且这样过长的链条会导致:
- 前几层的轻微扰动对最终结果的影响非常大;
- 梯度爆炸;
解决方案:我们只计算前几层的偏导数(截断时间步)
LSTM
Long Short-Term Memory, LSTM
模型结构

相对于 RNN 的传统结构,维护了长短两条线条以及三个门:
- 遗忘门:算出要遗忘多少长期记忆,,从 STM, Input 获得参数
- 输入门:其实感觉应该叫长期记忆更新门,从 STM, Input 获得参数
- 输出门:更新短期记忆,从 STM, Input 获得参数
前向传播
输入门:
遗忘门:
细胞状态:
即逐元素相乘。
中间状态:
隐藏状态更新方程:
输出层:
计算图

深层循环神经网络
获得更多非线性性

双向循环神经网络
填空,有点 bert 的感觉。
双向马尔可夫链。
体现了现代深度网络的设计原则: 首先使用经典统计模型的函数依赖类型,然后将其参数化为通用形式。
缺乏物理意义,仅在数学上可行,最终得到的是困惑度很低但是完全错误的文本。
编解码器 序列到序列学习(seq2seq)
传统模型无法有效处理 “输入和输出都为可变长序列”的任务
用“编码器”负责理解输入序列, 用“解码器”负责生成输出序列。
输入序列 x₁, x₂, ..., x_T ↓ Encoder ——→ 上下文向量 c ——→ Decoder ↓ 输出序列 y₁, y₂, ..., y_T'Encoder
使用循环神经网络(RNN / LSTM / GRU) 在每个时间步更新隐状态:
上下文变量通常选为最后时间步的隐状态:
若使用双向 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解码器
-
逐步生成输出序列(如翻译的法语句子)。
-
每个时间步的输出依赖于:
- 上一个时间步的输出(或真实标签,训练时使用)
- 编码器输出的上下文变量
- 自身的隐状态
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/