动手学深度学习9.3. 深度循环神经网络-笔记&练习(PyTorch)

发布于:2024-10-09 ⋅ 阅读:(7) ⋅ 点赞:(0)

本节课程地址:58 深层循环神经网络【动手学深度学习v2】_哔哩哔哩_bilibili

本节教材地址:9.3. 深度循环神经网络 — 动手学深度学习 2.0.0 documentation (d2l.ai)

本节开源代码:...>d2l-zh>pytorch>chapter_multilayer-perceptrons>lstm.ipynb


深度循环神经网络

到目前为止,我们只讨论了具有一个单向隐藏层的循环神经网络。 其中,隐变量和观测值与具体的函数形式的交互方式是相当随意的。 只要交互类型建模具有足够的灵活性,这就不是一个大问题。 然而,对一个单层来说,这可能具有相当的挑战性。 之前在线性模型中,我们通过添加更多的层来解决这个问题。 而在循环神经网络中,我们首先需要确定如何添加更多的层, 以及在哪里添加额外的非线性,因此这个问题有点棘手。

事实上,我们可以将多层循环神经网络堆叠在一起, 通过对几个简单层的组合,产生了一个灵活的机制。 特别是,数据可能与不同层的堆叠有关。 例如,我们可能希望保持有关金融市场状况 (熊市或牛市)的宏观数据可用, 而微观数据只记录较短期的时间动态。

图9.3.1 描述了一个具有 L 个隐藏层的深度循环神经网络, 每个隐状态都连续地传递到当前层的下一个时间步和下一层的当前时间步。

函数依赖关系

我们可以将深度架构中的函数依赖关系形式化, 这个架构是由 图9.3.1中描述了 L 个隐藏层构成。 后续的讨论主要集中在经典的循环神经网络模型上, 但是这些讨论也适应于其他序列模型

假设在时间步 t 有一个小批量的输入数据 \mathbf{X}_t \in \mathbb{R}^{n \times d}(样本数: n ,每个样本中的输入数: d )。 同时,将 l^\mathrm{th} 隐藏层(l=1,\ldots,L) 的隐状态设为 \mathbf{H}_t^{(l)} \in \mathbb{R}^{n \times h} (隐藏单元数: h ), 输出层变量设为 \mathbf{O}_t \in \mathbb{R}^{n \times q} (输出数: q )。 设置 \mathbf{H}_t^{(0)} = \mathbf{X}_t , 第 l 个隐藏层的隐状态使用激活函数 \phi_l ,则:

\mathbf{H}_t^{(l)} = \phi_l(\mathbf{H}_t^{(l-1)} \mathbf{W}_{xh}^{(l)} + \mathbf{H}_{t-1}^{(l)} \mathbf{W}_{hh}^{(l)} + \mathbf{b}_h^{(l)}), (9.3.1)

其中,权重 \mathbf{W}_{xh}^{(l)} \in \mathbb{R}^{h \times h} , \mathbf{W}_{hh}^{(l)} \in \mathbb{R}^{h \times h} 和 偏置 \mathbf{b}_h^{(l)} \in \mathbb{R}^{1 \times h}都是第 l 个隐藏层的模型参数。

最后,输出层的计算仅基于第 l 个隐藏层最终的隐状态:

\mathbf{O}_t = \mathbf{H}_t^{(L)} \mathbf{W}_{hq} + \mathbf{b}_q, (9.3.2)

其中,权重 \mathbf{W}_{hq} \in \mathbb{R}^{h \times q} 和偏置\mathbf{b}_q \in \mathbb{R}^{1 \times q}都是输出层的模型参数。

多层感知机一样,隐藏层数目 L 和隐藏单元数目 h 都是超参数。 也就是说,它们可以由我们调整的。 另外,用门控循环单元或长短期记忆网络的隐状态 来代替 (9.3.1) 中的隐状态进行计算, 可以很容易地得到深度门控循环神经网络或深度长短期记忆神经网络。

简洁实现

实现多层循环神经网络所需的许多逻辑细节在高级API中都是现成的。 简单起见,我们仅示范使用此类内置函数的实现方式。 以长短期记忆网络模型为例, 该代码与之前在 9.2节 中使用的代码非常相似, 实际上唯一的区别是我们指定了层的数量, 而不是使用单一层这个默认值。 像往常一样,我们从加载数据集开始。

import torch
from torch import nn
from d2l import torch as d2l

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

像选择超参数这类架构决策也跟 9.2节 中的决策非常相似。 因为我们有不同的词元,所以输入和输出都选择相同数量,即vocab_size。 隐藏单元的数量仍然是256。 唯一的区别是,我们现在(通过num_layers的值来设定隐藏层数)。

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)

[训练]与预测

由于使用了长短期记忆网络模型来实例化两个层,因此训练速度被大大降低了。

num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr*1.0, num_epochs, device)

输出结果:
perplexity 1.0, 27727.4 tokens/sec on cpu
time travelleryou can show black is white by argument said filby
travelleryou can show black is white by argument said filby

小结

  • 在深度循环神经网络中,隐状态的信息被传递到当前层的下一时间步和下一层的当前时间步。
  • 有许多不同风格的深度循环神经网络, 如长短期记忆网络、门控循环单元、或经典循环神经网络。 这些模型在深度学习框架的高级API中都有涵盖。
  • 总体而言,深度循环神经网络需要大量的调参(如学习率和修剪) 来确保合适的收敛,模型的初始化也需要谨慎。

练习

  1. 基于我们在 8.5节 中讨论的单层实现, 尝试从零开始实现两层循环神经网络。

解:
代码如下:

def get_params_2(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size

    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01

    # 隐藏层参数
    # 第一层
    W_xh = normal((num_inputs, num_hiddens))
    W_hh1 = normal((num_hiddens, num_hiddens))
    b_h1 = torch.zeros(num_hiddens, device=device)
    # 第二层
    W_hh2 = normal((num_hiddens, num_hiddens))
    b_h2 = torch.zeros(num_hiddens, device=device)
    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # 附加梯度
    params = [W_xh, W_hh1, b_h1, W_hh2, b_h2, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params

def init_rnn_state_2(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), 
            torch.zeros((batch_size, num_hiddens), device=device),)

def rnn_2(inputs, state, params):
    W_xh, W_hh1, b_h1, W_hh2, b_h2, W_hq, b_q = params
    H1, H2, = state
    outputs = []
    for X in inputs:
        H1 = torch.tanh(torch.mm(X, W_xh) + torch.mm(H1, W_hh1) + b_h1)
        H2 = torch.tanh(torch.mm(H1, W_hh2) + b_h2)
        Y = torch.mm(H2, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H1, H2,)
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_inputs = vocab_size
num_epochs, lr = 500, 1
net = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params_2,
                      init_rnn_state_2, rnn_2)
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)

输出结果:
perplexity 1.0, 41299.2 tokens/sec on cpu
time traveller for so it will be convenient to speak of himwas e
traveller with a slight accession ofcheerfulness really thi

2. 在本节训练模型中,比较使用门控循环单元替换长短期记忆网络后模型的精确度和训练速度。

解:
使用GRU和LSTM的perplexity都是1.0,应该是二者的模型复杂度足够cover这个小的数据集了;
训练速度上,在CPU上运行是LSTM更快一些。

gru_layer = nn.GRU(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr*1.0, num_epochs, device)

输出结果:
perplexity 1.0, 18433.6 tokens/sec on cpu
time travelleryou can show black is white by argument said filby
travelleryou can show black is white by argument said filby

3. 如果增加训练数据,能够将困惑度降到多低?

解:
训练数据增加8.5节练习2中的《世界大战》数据集,困惑度仍可以降到1.0。
代码如下:

import re

d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
                                '090b5e7e70c295757f55df93cb0a180b9691891a')
text = '/home/NAS/HUIDA/YaqinJiang/my/chapter_recurrent-neural-networks/36-0.txt'

with open(text, 'r') as f1, open(d2l.download('time_machine'), 'r') as f2:
    lines = f1.readlines() + f2.readlines()
for line in lines:
    re.sub('[^A-Za-z]+', ' ', line).strip().lower() 

def load_corpus(max_tokens=-1): 
    tokens = d2l.tokenize(lines, 'char')
    vocab = d2l.Vocab(tokens)
    corpus = [vocab[token] for line in tokens for token in line]

    if max_tokens > 0: 
        corpus = corpus[:max_tokens]
    return corpus, vocab

corpus, vocab = load_corpus()

class SeqDataLoader: 
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.seq_data_iter_sequential
        self.corpus, self.vocab = load_corpus(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps

    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

def load_data(batch_size, num_steps, use_random_iter=False, max_tokens=10000):
    data_iter = SeqDataLoader(
        batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab

batch_size, num_steps = 32, 35
train_iter, vocab = load_data(batch_size, num_steps)
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_inputs = vocab_size
num_epochs, lr = 500, 2
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

输出结果:
perplexity 1.0, 26658.8 tokens/sec on cpu
time traveller has enobont of the clockwork that kept the plane
traveller and its to derstly and intellects vast and cool a

4. 在为文本建模时,是否可以将不同作者的源数据合并?有何优劣呢?

解:
可以将不同作者的源数据合并,优劣如下:
优势:合并多个数据源可以显著增加训练数据的量,有助于构建更为鲁棒性和泛化能力更强的模型。
劣势:

  • 合并数据通常需要额外的预处理步骤,如统一文本格式、处理不同编码标准等,这会增加数据处理的复杂性。
  • 不同来源的数据质量可能参差不齐,一些数据可能包含错误、不规范的语言表达或噪声,这可能会影响模型的训练效果。
  • 不同作者的写作风格和内容可能相差较大,若某种风格或内容占比不足,可能影响模型的预测能力。