PyTorch 的 RNNs 实现

欢迎关注 “小白玩转Python”,发现更多 “有趣”

大多数类型的神经网络都是建立在对样本进行预测的基础上的,这些样本的目标都是神经网络训练过的。一个典型的例子是 MNIST 数据集。像 MLP 这样的常规神经网络知道有10个数字,它只是基于这些数字才能做出预测,即使图像与网络训练的图像非常不同。

现在,想象一下我们可以通过顺序分析利用这样一个网络,提供一个由9个有序数字组成的序列,让网络猜测第10个数字。这个网络不仅知道如何区分这10个数字,而且还知道,在0到8的序列中,下一个数字很可能是9。

当我们分析序列数据时,我们知道大多数情况下,序列中的元素是相互关联的,也就是说它们彼此依赖。正因为如此,我们需要考虑每个元素来理解序列的思想。

剑桥大学出版社(Cambridge University Press)将序列定义为“事物或事件相继发生的顺序”,或者,最重要的是,“一系列相关的事物或事件”。为了将这个定义调整到 Deep Learning 的范围内,sequence 是一组包含可训练上下文的数据,删除一些元素可能会使它变得无用。

但是一个序列包含什么呢?什么样的分组数据可以有上下文?我们如何提取背景来利用神经网络的力量?在进入神经网络本身之前,让我向您展示两类常用循环神经网络(RNNs)解决的问题。

时间序列预测

第一个例子是时间序列预测问题,我们用现有的数值序列(蓝色)来训练神经网络,以便预测未来的时间步长(红色)。

如果我们把一个家庭多年来每月的用电量消费按顺序排列,我们可以看到有一个上升的正弦趋势,同时有一个突然的下降。

正弦部分的上下文可能是一年中从夏季到冬季和从夏季再到夏季的不同电量需求。不断增长的电量消耗可能来自于使用更多的电器和设备,或者转向需要更多电量的功率更大的设备。这种突然的下降可能意味着一个人已经长大到足以离开家,而那个人所需要的电量现在已经不需要了。

您对上下文的理解越好,就可以向网络提供更多的信息,以帮助它理解数据,通常是通过连接输入向量来实现的。在这种情况下,对于每个月的数据而言,我们可以分为三个部分:电器和设备的数量、它们的电量效率和家庭所容纳的人口数量。

自然语言处理

玛丽骑着自行车,自行车是_____。

第二个例子是自然语言处理问题。这也是一个很好的例子,因为神经网络必须考虑现有句子所提供的上下文来完成它。

假设我们的网络被训练成用所有代词完成句子。一个训练有素的网络会理解这句话是建立在第三人称单数和玛丽很可能是一个女性的名字。所以,预测的代词应该是“她的”,而不是阳性的“他的”或复数的“他们的”。

现在我们已经看到了两个有序数据的示例,接下来让我们探索网络的前向和反向传播。

RNN 配置

正如我们所看到的,RNNs 从序列中提取信息以提高其预测能力。

简单的循环网络图

一个简单的 RNN 图表示在上面。绿色节点允许输入一些 x^t,并输出一些值 h^t,这些值也会反馈给包含从输入收集的信息的节点。无论什么样的模式输入到节点,节点都会学习它,并保存这些信息以备下一次输入。上标 t 代表时间步长。

 循环网络配置

根据输入或输出的形状,神经网络的配置有一些变化,稍后我们将了解节点内部发生了什么。

Many-to-one 配置是当我们在不同的时间步骤中输入多个输入以获得一个输出,这可能是在一个电影场景的不同帧中捕获的情感分析。

One-to-many 使用一个输入来获得多个输出。例如,我们可以使用 many-to-one 的结构编码一首表达某种情感的诗歌,并使用 one-to-many 的结构来创造具有同样情感的诗歌。

Many-to-many 使用多个输入来获得多个输出,比如使用一系列值,比如在电量预测使用中,预测未来12个月而不是只使用一个。

Stacked many-to-many 就是一个包含多个隐藏层节点的网络。

RNN 前向传播

为了理解神经网络节点内部发生的情况,我们将使用一个简单的数据集作为时间序列预测的示例。下面是完整的序列值及其重组作为一个训练和测试数据集。

现在,让我们将数据集分成若干批。

我没有在这里显示它,但是不要忘记数据集应该被规范化处理。这是重要的,因为神经网络是敏感的数据集值的大小。

这个想法是预测未来的一个值。所以,假设我们选择批处理的第一行: [10 20 30] ,经过训练我们的网络后,我们应该得到值40。为了测试神经网络,我们可以feed向量[70 80 90] ,并期望得到一个接近100,如果网络是训练有素的。

我们将使用多对一配置分别提供每个序列的三个时间步骤。当使用循环网络时,输入不是进入网络的唯一值,还有一个隐藏数组,它是将序列的上下文从一个节点传输到另一个节点的结构。我们将其初始化为一个零数组,并将其连接到输入。它的维数(1 x 2)是一个个人选择,只是使用了与1 x 1步骤输入不同的维数。

循环前向传播

仔细观察,我们可以看到权重矩阵被分成两部分。第一个处理创建两个输出的输入,第二个处理创建两个输出的隐藏数组。然后将这两组输出合并在一起,获得一个新的隐藏数组,其中包含来自第一个输入(10)的信息,并将其提供给下一个时间步骤输入(20)。应该注意的是,从时间步长到时间步长,权重矩阵和偏差矩阵是相同的。

全局输入向量 x^t、权重矩阵 w 和偏置矩阵 b 以及隐数组的计算都在上面进行了表示。只有一个步骤没有完成,那就是使用最后一个隐藏数组来预测未来的下一个时间步骤,用一个线性层来计算最终结果。整个网络的形式如下:

你能看到多对一的配置吗?我们从一个序列中提供三个输入,它们的上下文被权重和偏差矩阵捕获,并存储在一个隐藏数组中,在每个时间步骤中更新新的信息。最后,存储在最终隐藏数组中的上下文经过另一组权重和偏差,在将序列的所有时间步长输入网络后输出一个值。

我们可以看到最终隐状态的线性形式,线性层的权重和偏差矩阵以及预测值的计算。

RNN的反向传播

在神经网络的训练中,反向传播是一个非常重要的步骤。在这里,预测输出和实际值之间的误差传播到神经网络,目的是改善权重和偏差,以获得更好的预测与每次迭代。

大多数时候,这个步骤因为它的复杂性而被忽略。在提到重要的部分时,我会尽可能简单地向你解释。

反向传播是一系列的导数,使用链式规则的微积分计算损失以及所有的权重和偏差参数。这意味着我们最终需要以下值(如果它们是多维的,则需要数组) :

如果你不熟悉一阶导数的数学意义,我建议你读一读梯度下降法,但本质上,当一阶导数为零时,通常意味着我们在系统中找到了一个最小值,理想情况下我们不能进一步改进它。

这里需要注意一点:零点也可以是最大值,它是不稳定的,优化不应该到那里,或者鞍点本身也不是很稳定的位置。最小值可以是全局的(函数的最小值) ,也可以是局部的。

梯度下降法的例子:两个球滚下山

你在图片中看到的是两个滚下山谷的球。从视觉上看,一阶导数给我们一个山坡倾斜度的大小。如果我们沿着 w 轴增加的方向(从左到右)走,那么对于绿色的球,倾角是负的(向下走) ,对于红色的球,倾角是正的(向上走)。

如果我们希望损失最小,我们希望球到山谷的最低点。W 代表权重和偏差的值,所以如果我们在绿色球的位置,我们将减去负导数的一部分(使其为正)到绿色球向右移动的 w 位置,减去正导数的一部分(使其为负)到红色球向左移动的 w 位置,以便接近两个球的最小值。

从数学上来说,我们得到的结果如下:

η 调整了我们用来更新权重和偏差的导数的比例。

现在,继续讨论反向传播的问题。我会给出从损失到所有参数的链式导数,我们会看到每个导数代表什么。重要的是要有方程从上面提出的层连同他们的参数矩阵在脑海中。

需要记住的一点是,我们正在寻找的四个一阶导数数组的形状必须与我们将要更新的参数相同。例如,数组 dL/dW_h 的形状必须与权重数组 W_h 相同。上标 T 表示矩阵是转置的。

我们找到了最终线性层参数的导数。我们知道 dL/dy_hat 的形状是1 x 1和 h^3 2 x 1,而且矩阵乘法的内部维度必须是一致的,所以 h^3需要转置。我们得到一个矩阵是1 × 2,因为 W_h 是2 × 1,我们需要第二个转置。

现在,反向传播过程稍有不同。尽管所有三个时间步骤中的参数都是相同的,但我们仍然通过所有这些参数进行反向传递,对于每个隐藏节点,我们计算一个新的 dL/dW_x 和 dL/dB_x。

分母中的数字上标与时间步长有关。我们计算了第三个时间步长的参数,现在我们需要回到第二个时间步长。

我们回到了 dL/dh^2,这意味着我们回传了一个完整的循环隐藏节点,为此我们需要从全局输入导数中去连接输入和隐藏状态。接下来的两个隐状态的计算是相同的,所以我不会详细讨论它们。

现在我们把这些导数加在一起,应用我们之前看到的梯度下降法方程来更新参数,这个模型就可以进行另一次迭代了。让我们看看如何用 PyTorch 构建一个简单的 RNN。

PyTorch 的 RNN 实现

使用 PyTorch 使它变得非常简单,因为我们真的不需要担心反向传播。然而,我仍然相信了解它的工作原理是很重要的,即使我们不直接使用它。

接下来,如果我们参考 PyTorch 的文档,我们可以看到他们已经准备好了一个 RNN 对象可以使用。我将留下一段代码,我实现的 RNN 和如何训练它。不要忘记在使用网络之前对数据进行规范化,并创建一个数据集和一个数据引导器。

import torch
import torch.nn as nn


class RNN_LSTM_Base(nn.Module):
    def training_step(self, batch):
        samples, targets = batch
        outputs = self(samples.double())
        loss = nn.functional.mse_loss(outputs, targets)
        return loss


class VanillaRNN(RNN_LSTM_Base):
    def __init__(self, in_size, hid_size, out_size, n_layers=1):
        super(VanillaRNN, self).__init__()        
        # Define dimensions for the layers
        self.input_size = in_size
        self.hidden_size = hid_size
        self.output_size = out_size
        self.n_layers = n_layers        
        # Defining the RNN layer
        self.rnn = nn.RNN(in_size, hid_size, n_layers, batch_first=True)        
        # Defining the linear layer
        self.linear = nn.Linear(hid_size, out_size)
        
    def forward(self, x):
        # x must be of shape (batch_size, seq_len, input_size)
        xb = x.view(x.size(0), x.size(1), self.input_size).double()        
        # Initialize the hidden layer's array of shape (n_layers*n_dirs, batch_size, hidden_size_rnn)
        h0 = torch.zeros(self.n_layers, x.size(0), self.hidden_size_rnn, requires_grad=True).double()        
        # out is of shape (batch_size, seq_len, num_dirs*hidden_size_rnn)
        out, hn = self.rnn(xb, h0)        
        # out needs to be reshaped into dimensions (batch_size, hidden_size_lin)
        out = nn.functional.tanh(hn)        
        # Finally we get out in the shape (batch_size, output_size)
        out = self.linear(out)
        return out




def fit(epochs, lr, model, train_loader, test_loader, opt_func=torch.optim.SGD):
    optimizer = opt_func(model.parameters(), lr)
    for epoch in range(epochs):
        # Training phase
        model.train()
        for batch in train_loader:
            loss = model.training_step(batch)
            # Calculate gradients from chain rule
            loss.backward()
            # Apply gradient descent step
            optimizer.step()
            # Remove gradients for next iteration
            optimizer.zero_grad()
    return 'Trained for {} epochs'.format(epochs)


@torch.no_grad()
def predict(model, dataloader):
    result = []
    for dl in dataloader:
        sample, target = dl
        output = model(sample.double())
        result.append([output.item(), target.item()])
    return result

最后的想法

最后,我们简要总结了这里所讨论的内容,我们首先看到两种通常用循环网络来解决的问题,即时间序列预测和自然语言处理。

后来我们看到了一些典型配置的例子和一个实际例子,其目的是使用多对一配置预测未来的一个时间步骤。

在前向传播中,我们了解了输入和隐状态如何与循环层的权重和偏差相互作用,以及如何利用最后隐状态中包含的信息来预测下一个时间步长的值。

反向传播就是链式规则的应用,从损失梯度到预测,直到它变成我们想要优化的参数。

最后,我们展示了一段使用 PyTorch 构建一个基本的循环网络的代码。

·  END  ·


HAPPY LIFE


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值