动手学深度学习(李沐)——现代循环神经网络读书笔记

矩阵连续乘积可以导致梯度消失或梯度爆炸的问题。这导致我们的循环网络不能太长,但是在循环神经网络中早期观测值对预测所有未来观测值可能具有非常重要的意义,同时一些词元也可能没有相关的观测值。这里我们介绍以下现代循环神经网络解决这些问题。

1.门控循环单元(GRU)

1.1重置门和更新门

我们先介绍用法,之后再讲原理。

其中 Wxr,Wxz∈Rd×h和 Whr,Whz∈Rh×h是权重参数,br,bz∈R1×h是偏置参数。重置门Rt​∈Rn×h 和 更新门 Zt∈Rn×h。

我们先只要知道通过这俩个式子可以得到这俩个矩阵(门)

1.2候选隐状态和隐状态

候选隐状态公式如下:

与普通RNN相比, (9.1.2)中的 Rt和 Ht−1的元素相乘可以减少以往状态的影响。每当重置门 Rt中的项接近1时,我们恢复成普通的循环神经网络。对于重置门 Rt 中所有接近 0 的项,候选隐状态是以 Xt作为输入的多层感知机的结果。因此,任何预先存在的隐状态都会被重置为默认值。

隐状态:

这里,我们把俩个门元素设置成0到1数值,可以很好理解门的作用:重置门允许我们控制"可能还想记住"的过去状态的数量; 更新门将允许我们控制新状态中有多少个是旧状态的副本。

1.3从零开始实现

复制代码
def gru(inputs, state, params):
    W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    for X in inputs:
        Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
        R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
        H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
        H = Z * H + (1 - Z) * H_tilda
        Y = H @ W_hq + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)

1.4简洁实现

复制代码
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

2.长短期记忆网络(LSTM)

2.1输入,输出,遗忘门

其中一个门用来从单元中输出条目,我们将其称为输出门 (output gate)。 另外一个门用来决定何时将数据读入单元,我们将其称为输入门 (input gate)。 我们还需要一种机制来重置单元的内容,由遗忘门(forget gate)来管理, 这种设计的动机与门控循环单元相同, 能够通过专用机制决定什么时候记忆或忽略隐状态中的输入

2.2候选记忆单元

2.3记忆元

流程图片如图:

2.4.隐状态

2.5从零实现

复制代码
def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
     W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
        F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
        O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
        C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * torch.tanh(C)
        Y = (H @ W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H, C)

2.6简洁实现

复制代码
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

3.深度循环神经网络

简单使用:以LSTM为例,使用的时候加入层数就可以了。

复制代码
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)

4.序列到序列(seq2seq)

以英文翻译成法语为例,模型分为编码器和解码器,编码器处理原序列(英文),提前原序列整个序列的信息存储在隐状态中,解码器用于处理目标序列(法语),编码器最终的隐状态(最后一层,最后一个时间步)在每一个时间步都作为解码器的输入序列的一部分。同时作为初始隐状态。

4.1编码器

复制代码
#@save
class Seq2SeqEncoder(d2l.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)

    def forward(self, X, *args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)
        # 如果未提及状态,则默认为0
        output, state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state的形状:(num_layers,batch_size,num_hiddens)
        return output, state

4.2解码器

复制代码
class Seq2SeqDecoder(d2l.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        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, *args):
        return enc_outputs[1]

    def forward(self, X, state):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1, 0, 2)
        # 广播context,使其具有与X相同的num_steps
        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)
        # output的形状:(batch_size,num_steps,vocab_size)
        # state的形状:(num_layers,batch_size,num_hiddens)
        return output, state

4.3合并编码器和解码器

复制代码
#@save
class EncoderDecoder(nn.Block):
    """编码器-解码器架构的基类"""
    def __init__(self, encoder, decoder, **kwargs):
        super(EncoderDecoder, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, enc_X, dec_X, *args):
        enc_outputs = self.encoder(enc_X, *args)
        dec_state = self.decoder.init_state(enc_outputs, *args)
        return self.decoder(dec_X, dec_state)

4.4损失函数

复制代码
#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""
    # pred的形状:(batch_size,num_steps,vocab_size)
    # label的形状:(batch_size,num_steps)
    # valid_len的形状:(batch_size,)
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)//有效长度内为1
        self.reduction='none'
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
            pred.permute(0, 2, 1), label)
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss

流程图:

4.5 训练

复制代码
#@save
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""
    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])

    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    loss = MaskedSoftmaxCELoss()
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                     xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # 训练损失总和,词元数量
        for batch in data_iter:
            optimizer.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
                          device=device).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()      # 损失函数的标量进行“反向传播”
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
        f'tokens/sec on {str(device)}')

4.6预测

以bos字符作为开头,结合编码输出的最后一层最后一个的隐状态开始预测,预测到eos结束。

复制代码
#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为评估模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    # 添加批量轴
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

4.7序列评估

训练的时候我们需要用损失函数,损失函数注重的是单个词汇,但是在评估整个序列时,我们看的是序列的整体好坏,我们需要新的评估标准。这里我们采用BLUE评估标准。

这里我们先举个例子,给定序列:ABCDEF。模型预测序列:ABBCD。我们通过BLUE衡量好坏。

代码如下:

k 决定了要计算 1-gram 到 k-gram 的精确率(precision),然后把这些阶的分数加权连乘起来。

复制代码
def bleu(pred_seq, label_seq, k):  #@save
    """计算BLEU"""
    pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[' '.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score

4.8束搜索

在预测序列时我们逐个预测输出序列, 直到预测序列中出现特定的序列结束词元"<eos>"。在之前的代码中,对于输出序列的每一时间步(t′),我们都将基于贪心搜索从 YY中找到具有最高条件概率的词元,即:

但是当前最佳并不一定是全局最佳。 这里我们介绍另外一种算法:束搜索

比如k=2,选择情况如下:

也就是保留 Top-k 个备胎。每一步同时保留概率最大的 k 个(即束宽)候选序列,当然这也不一定是全局最优,但是相对较好,且时间复杂度较低。

相关推荐
白露与泡影3 小时前
从 GLM 到 GPT,HarnessPro 如何用 RSI 降低 Agent 成本
人工智能·gpt·机器学习
u1301303 小时前
GitHub 热榜项目:日榜(2026-10-28)
人工智能·github
会编程的吕洞宾3 小时前
RAG检索质量怎么评估?3个指标+LLM裁判实战
java·开发语言·人工智能·后端
ShuangNaoPai3 小时前
失效检测与原型匹配:基于双脑派于华的高尔夫案例,重构AI内容生成策略
人工智能·架构·产品运营·prompt
西柚不烦恼3 小时前
Windows 本地部署私有 AI 全攻略:纯 CPU 环境(Ollama + Open WebUI)
人工智能·windows
2601_962885723 小时前
让 AI Agent 自动收盘复盘:定时取全市场数据 + 生成复盘
人工智能
2501_918126913 小时前
如何使用ai预防ai爆破服务器
人工智能
马剑威(威哥爱编程)3 小时前
【HarmonyOS 7 小艺智能体实战】10:提交、审核、上架与上线后——把服务真正交给用户
人工智能·华为·harmonyos
yyk333243 小时前
YOLOv3算法
人工智能·yolo·目标跟踪
cooldream20093 小时前
AI 时代编程学习路线图:四个阶段,从零基础到独立交付
人工智能·编程学习