【AI核心能力】第8讲 RNN:依赖问题与神经网络的“前世今生”

第8讲 循环神经网络 RNN

时间序列、文本、依赖问题 && Time Series, Text, Dependency Problems

在现实生活中,我们会遇到很多跟时间序列相关的问题,比如一个公司的交易情况或者收入情况等

image-20230920164125230

我们还会遇到和上下文相关的文本处理问题,体现在每个的含义根据在句中位置的不同而有不同的含义

image-20230920164234937

这就是在编译原理中,一个很重要的概念——有限状态机(Finite State Machines)

有限状态机可以模拟很多事情,我们每天都在用的编译器其实就是个有限状态机的应用实例,它能够接收一行行输入的程序语言来产生各种各样的行为,那么在此时此刻它的行为是什么样的,这就是由有限状态机来决定的。

image-20230920165440594

序列问题 Sequence Problems

序列问题分为以下好几类情况

image-20230920165518250

如何区分一对一 (one to one) 和一对多 (one to many) 问题呢?比如,我们输入一张小猫图片,最后输出

  • pic -> <0.1, 0.2, 0.1, 0.3, 0.3, 0.1, 0.3> —— one to one,因为输出的这些值之间没有相关性;
  • pic -> <这是一个哈基米>—— one to many,输出值 (每个字) 之间具有相关性,一个字是否出现、出现在什么位置,与前后文本相关。

还有多对一 (many to one) 问题

  • <今天不开心> -> -1 —— Negative值
  • <今天很不开心> -> -3 —— Negative值

多对多 (many to many) 问题

  • <Knowledge is power> -> <知识就是力量>——前后都有依赖关系

也就是说,生活中的所有依赖问题/序列问题都可以抽象成上述几类。

用PyTorch搭建全连接神经网络并解决一个序列问题

我们现在有这样的一些数据,是一些公司连续两月的营收(数据集后续上传至github)

image-20230921143902089

我们希望能对某公司未来的营收进行预测。

import pandas as pd

timeseries_revenue = pd.read_csv('time_serise_revenue.csv')

timeseries_revenue = timeseries_revenue.drop(timeseries_revenue.columns[0], axis=1)
timeseries_revenue

image-20230921144413559

我们选其中一个公司看看

timeseries_revenue.sample()

image-20230921144550664

import matplotlib.pyplot as plt

plt.plot(timeseries_revenue.sample().values[0])

image-20230921144635723

假设我们现在已知10天的营收,要预测第11天的营收

一般我们会这么设置训练样本

每个x是前10天的营收,target数据y是第11天的数据

但实际工作中,为了更好的效果,我们会这么设置

每个x是第i天到第i+9这十天的数据,target数据y是第i+1到第i+10的数据

搭建网络

接下来我们就用PyTorch框架来搭建一个简单的深度学习网络。

import torch
import torch.nn as nn

import numpy as np

class FullyConnected(nn.Module):  # 可以看到每个Module都是PyTorch的一个子类
    def __init__(self, x_size, hidden_size, output_size):
        super(FullyConnected, self).__init__()  # 调用了父类方法进行初始化(还记得我们之前手搓神经网络时的定义吗)
        self.hidden_size = hidden_size

        self.linear_with_tanh = nn.Sequential(
            nn.Linear(x_size, self.hidden_size),
            nn.Tanh(),
            nn.Linear(self.hidden_size, self.hidden_size),
            nn.Tanh(),
            nn.Linear(self.hidden_size, output_size)
        )

    def forward(self, x):
        yhat = self.linear_with_tanh(x)
        return yhat

这是一个全连接的网络 (fully connected) 这里要注意的就是要正确设置中间层的层数,比如我们的输入维度是n*10,输出也是n*10,中间一共4层,那么就可以将中间层分别设置为10*88*88*99*10

fc_model = FullyConnected(x_size=10, hidden_size=8, output_size=10)

前向传播

下面我们做个简单的实验,看看这个神经网络是否能将[1, 2, … , 10]前向传播后输出一个1*10的结果

some_test = np.array([[i for i in range(10)]])

fc_model(torch.from_numpy(some_test).float())  # 注意这里要将np数组转换成一个TENSOR张量

image-20230921152341703

这就是随机设置W时前向传播得到的数据,确实是1*10的大小。

Python 的魔法方法 __call__

这里可能会有人觉得奇怪,为什么实例化一个fc_model之后就会自动调用forward呢?
原因就是在 PyTorch 的 nn.Module 类中,实际上重写了 __call__ 方法,以便在模型对象上调用时能够执行 forward 方法。

下面是一个__call__方法使用的经典例子

class FindPartner:
    def __init__(self, name):
        self.name = name

    def __call__(self, another):
        return self.say_hello(another)
    
    def say_hello(self, another):
        print('{}快来,我是{}'.format(another, self.name))
        
findpartner = FindPartner('老刘')
findpartner('老张')

### Output:
### 老张快来,我是老刘

梯度下降

接下来我们开始梯度下降,试图通过100次迭代将0~9拟合成1~10

from torch import optim

criterion = nn.MSELoss()
optimizer = optim.SGD(fc_model.parameters(), lr=0.01)

losses = []
for iter_ in range(100):
    inputs = torch.from_numpy(some_test).float()  # 0 ~ 9
    targets = torch.from_numpy(some_test+1).float()  # 1 ~ 10
    outputs = fc_model(inputs)

    loss = criterion(outputs, targets)
    optimizer.zero_grad()

    loss.backward()
    optimizer.step()

    losses.append(loss)
losses

image-20230921162614386

能看到loss值一直在下降

# 使用detach()方法移除梯度信息,然后再转换为NumPy数组
losses_np = [loss.detach().numpy() for loss in losses]

plt.plot(losses_np)

image-20230921162631483

fc_model(torch.from_numpy(np.array([0,1,2,3,4,5,6,7,8,9])).float())

image-20230921162710651

最后输入的1~9非常接近1~10了。(不过此时输入其他数字都会输出1~10,原因是训练数据太少了发生了过拟合)

训练

理解了PyTorch每步做了什么,我们把真实数据送进去训练看看。

首先定义一个生成训练集的函数,做法是从DataFrame中随机选择一行,并从该行中随机选择一个起始列索引begin_column,然后返回从该列开始的连续sample_size个值以及其后的sample_size个值。

import random

def sample_from_table(sample_size, dataframe):
    sample_row = dataframe.sample().values[0]
    
    begin_column = random.randint(0, len(sample_row) - sample_size - 1)  # 这么取值是为了防止越界
    
    return (sample_row[begin_column: begin_column + sample_size], 
            sample_row[begin_column + 1: begin_column + sample_size + 1])
fc_model = FullyConnected(x_size=10, hidden_size=3, output_size=10)
fc_model = fc_model.double()

criterion = nn.MSELoss()

optimizer = optim.SGD(fc_model.parameters(), lr=0.01)

n_epochs = 100
n_iters = 50
# losses = []
losses = np.zeros(n_epochs)  # 每个epoch记录一个loss

for epoch in range(n_epochs):
    for iter_ in range(n_iters):
        inputs_, targets_ = sample_from_table(50, timeseries_revenue)
        
		# 这里我们一次送入5批数据进行训练,这样可以加快训练速度
        inputs = torch.from_numpy(np.array([inputs_[:10],
                                            inputs_[10:20],
                                            inputs_[20:30],
                                            inputs_[30:40],
                                            inputs_[40:50]],
                                            dtype=np.double))

        targets = torch.from_numpy(np.array([targets_[:10], 
                                            targets_[10:20], 
                                            targets_[20:30], 
                                            targets_[30:40], 
                                            targets_[40:50]],
                                            dtype=np.double))

        outputs = fc_model(inputs.double())

        loss = criterion(outputs, targets)
        optimizer.zero_grad()

        loss.backward()
        optimizer.step()

        losses[epoch] += loss

        if iter_ % 10 == 0:
            plt.clf()
            plt.ion()
            plt.title("Epoch {}, iter {}".format(epoch, iter_))
            plt.plot(torch.flatten(outputs.detach()),'r-',linewidth=1,label='Output')  # 拟合得到结果
            plt.plot(torch.flatten(targets),'c-',linewidth=1,label='Label')  # 目标结果
            plt.plot(torch.flatten(inputs),'g-',linewidth=1,label='Input')  # 输入数据(目标结果沿时间轴平移-1)
            plt.draw()
            plt.pause(0.05)

下面是第一轮epoch前10次迭代的结果,可以看到Output基本是乱来的

image-20230922222146742

但是到了第40轮epoch结束,似乎学得有点样子了

image-20230922222321742

训练结束时,模型基本已经能够预测出下一天的营收了(效果其实还不错了,注意看纵轴标度和最开始不一样)。

image-20230922222432883

我们打印一下loss值看看

losses[-10:]

image-20230922222857275

plt.plot(losses)

image-20230922222806285

可以调一下学习率和模型层数看看能不能有更低的loss。

效果不好的情况

RNN 并不适用于解决那些 “此刻(t)数据与之前(0~t-1)数据没有明显关系的问题”。

这里我们导入一个稍微复杂一点的数据集——time_serise_sale,是每天的销售额

timeseries_sales = pd.read_csv('time_serise_sale.csv')

timeseries_sales = timeseries_sales.drop(timeseries_sales.columns[0], axis=1)
timeseries_sales

image-20230922225038557

plt.plot(timeseries_sales.sample().values[0])

image-20230922225200878

如果我们再用上面的模型对它进行预测,会发现模型预测得很“挣扎”

image-20230922225625316

我们也是打印一下loss看看

plt.plot(losses)

image-20230922230144976

别被大趋势所呈现出的“假象”所迷惑,事实上第10次epoch后我们的梯度下降是没卵用的

plt.plot(losses[10:])

image-20230922230334300

也就是说,传统的全连接网络在处理上述问题时是存在缺陷的。

RNN 循环神经网络

循环神经网络

说了这么多终于,来到了我们这篇文章的主角——RNN 循环神经网络

在传统神经网络中,我们这么定义中间层
y = σ ( w x + b ) y=\sigma(wx+b) y=σ(wx+b)
如果x是一个2*4的向量,那么就有
( 1 2 3 4 5 6 7 8 ) ˙ ( a 1 a 2 a 3 a 4 a 5 a 6 a 7 a 8 ) \begin{pmatrix} 1&2&3&4\\ 5&6&7&8 \end{pmatrix} \dot{} \begin{pmatrix} a_1&a_2\\a_3&a_4\\ a_5&a_6\\a_7&a_8 \end{pmatrix} (15263748)˙ a1a3a5a7a2a4a6a8
但是在这个计算过程中,我们并没有把前后层之间的依赖关系纳入考虑。

那么怎么将前后关系纳入考量呢?

网络结构

有人就发明了下面这种网络结构,从而让计算x^(t)的时候将x^(t-1)的信息也记录下来

image-20230923115143432

其中,x^(t)就可以理解为上面所说第9天的数据,那么x^(t-1)x^(t+1)就分别是第8天和第10天的。

数学表示

如果从数学上表示这种关系,那么

  • 先前我们这么定义神经网络:
    y t = σ ( W x t + b ) y t + 1 = σ ( W x t + 1 + b ) y_t = \sigma(Wx_t+b)\\ y_{t+1} = \sigma(Wx_{t+1}+b) yt=σ(Wxt+b)yt+1=σ(Wxt+1+b)

  • 现在我们重新定义神经网络:

    • Elman networks
      h t = σ h ( W h x t + U h h t − 1 + b h ) y t = σ y ( W y h t + b y ) h_t = \sigma_h(W_hx_t+U_hh_{t-1}+b_h)\\ y_t = \sigma_y(W_yh_t+b_y) ht=σh(Whxt+Uhht1+bh)yt=σy(Wyht+by)

    或者

    • Jordan networks
      h t = σ h ( W h x t + U h y t − 1 + b h ) y t = σ y ( W y h t + b y ) h_t = \sigma_h(W_hx_t+U_hy_{t-1}+b_h)\\ y_t = \sigma_y(W_yh_t+b_y) ht=σh(Whxt+Uhyt1+bh)yt=σy(Wyht+by)

    这两种定义方式都通过h_t的计算包含了依赖关系,前者是在计算y_t的时候包含了上一次的权重h_{t-1},后者包含了y_{t-1}。从全过程来看,理论上此刻的y_t将与先前所有的x都相关。

训练

我们首先定义一个简单的循环神经网络

参数n_layers指的是有几层循环神经网络叠加,下图是一个两层叠加的图例
image-20230923154357671

class RecurrentModel(nn.Module):
    def __init__(self, x_size, hidden_size, n_layers, batch_size, output_size):
        super(RecurrentModel, self).__init__()

        self.hidden_size = hidden_size
        self.n_layers = n_layers
        self.batch_size = batch_size

        self.rnn = nn.RNN(x_size, hidden_size, n_layers, batch_first=True)

        self.out = nn.Linear(hidden_size, output_size)


    def forward(self, inputs, hidden=None):
        hidden = self.init_hidden()

        output, hidden = self.rnn(inputs.float(), hidden.float())
        output = self.out(output.float())
		# 记得循环神经网络需要返回y和h两个值用于下一步计算
        return output, hidden
    

    def init_hidden(self):
        hidden = torch.zeros(self.n_layers, self.batch_size, self.hidden_size, dtype=torch.double)  # 这里需要注意一下顺序
        return hidden

接着,参考全连接网络一样构建训练代码,为了比较时的公平,将hidden_size也设置成8层

PyTorch中的unsqueeze函数:

用于在张量(Tensor)的特定维度上增加一个新的维度,作用是改变张量的形状,使其维度增加。
unsqueeze 接受一个整数参数,该参数指定在哪个维度上插入新的维度。例如,如果我们有一个一维张量 tensor,形状为 (n,),那么可以使用 unsqueeze 将其变成一个二维张量,形状为 (n, 1),如下所示:

import torch
# 创建一个一维张量
tensor = torch.tensor([1, 2, 3])
# 使用unsqueeze在第二维度上插入一个新维度
new_tensor = tensor.unsqueeze(1)
print(new_tensor)

### Output:
### tensor([[1],
###         [2],
###         [3]])

在上述示例中,unsqueeze(1) 在第二维度上插入了一个新维度,将原始一维张量变成了一个二维张量。或者我们在第二个维度上插入新的维度

torch.from_numpy(np.array([[1,2,3], [2,3,4]])).unsqueeze(2)

### Output:
### tensor([[[1],
###          [2],
###          [3]],
### 
###         [[2],
###          [3],
###          [4]]], dtype=torch.int32)

unsqueeze函数在处理输入数据的形状时非常有用,特别是在深度学习中,因为有些模型需要特定形状的输入数据。

在下面代码中,unsqueeze(2) 的作用是将形状为 (batch_size, seq_length) 的二维张量变成了形状 (batch_size, seq_length, 1) 的三维张量,这是因为 RNN 模型通常需要三维的输入,其中一个维度表示时间步(sequence length),用于告诉模型现在进行到时间步中的哪一步(注意理解 RNN 网络需要一个一个节点顺序输入,而不是像全连接网络一样以1*10的向量为单位整个输入)。

# fc_model = FullyConnected(x_size=10, hidden_size=3, output_size=10)

x_size = 1
hidden_size = 8
n_layers = 2
batch_size = 5
seq_length = 10  # 我们期望获得的数据长度
n_sample_size = 50  # 一次输入的数据个数
output_size = 1
rnn_model = RecurrentModel(x_size, hidden_size, n_layers, int(n_sample_size/seq_length), output_size)
rnn_model = rnn_model.float()  # 这里的模型要和下面inputs的类型对应上

criterion = nn.MSELoss()
optimizer = optim.SGD(rnn_model.parameters(), lr=0.01)

n_epochs = 100
n_iters = 50
# losses = []
losses = np.zeros(n_epochs)  # 每个epoch记录一个loss

hidden = rnn_model.init_hidden()
# hidden = None

for epoch in range(n_epochs):
    for iter_ in range(n_iters):
        inputs_, targets_ = sample_from_table(50, timeseries_sales)
        
		# 这里我们一次送入5批数据进行训练,这样可以加快训练速度
        inputs = torch.from_numpy(np.array([inputs_[:10],
                                            inputs_[10:20],
                                            inputs_[20:30],
                                            inputs_[30:40],
                                            inputs_[40:50]],
                                            dtype=np.float32)).unsqueeze(2)

        targets = torch.from_numpy(np.array([targets_[:10], 
                                            targets_[10:20], 
                                            targets_[20:30], 
                                            targets_[30:40], 
                                            targets_[40:50]],
                                            dtype=np.float32)).unsqueeze(2)

        outputs, _ = rnn_model(inputs.double(), hidden.double())

        loss = criterion(outputs, targets.float())
        optimizer.zero_grad()

        loss.backward()
        optimizer.step()

        losses[epoch] += loss

        if iter_ % 10 == 0:
            plt.clf()
            plt.ion()
            plt.title("Epoch {}, iter {}".format(epoch, iter_))
            plt.plot(torch.flatten(outputs.detach()),'r-',linewidth=1,label='Output')  # 拟合得到结果
            plt.plot(torch.flatten(targets),'c-',linewidth=1,label='Label')  # 目标结果
            plt.plot(torch.flatten(inputs),'g-',linewidth=1,label='Input')  # 输入数据(目标结果沿时间轴平移-1)
            plt.draw()
            plt.pause(0.05)

看看结果,这是最开始

image-20230923160653592

才第9个epoch就学得有点样子了

image-20230923160749958

同样是第40轮的表现,相比于全连接模型的“挣扎”,RNN 的表现要好太多

image-20230923160832288

最后学成了这样

image-20230923161001988

我们再来看看loss的下降

plt.plot(losses)

image-20230923161215863

同样放大检查一下

plt.plot(losses[10:])

image-20230923161258904

的确是在有效下降。

注:对于第一个简单的数据集,RNN 的效果也会更好,基本10个epoch就能搞定了。

改进

事实上,当我们在做文本翻译的问题时,一个单词的含义不仅和其前面一个字 (所有字) 有关,还跟其后面一个字 (所有字) 有关。

所以我们在使用 RNN 的时候,还可以把后一个x值包含到当前节点的输入里(尽管它实在后一时刻产生的)。这时我们的 RNN 就从Stacked RNN变成了Bidirectional RNN,PyTorch中也内置了bidirectional参数用于双向传播。

挑战 The Challenge of Long-Term Dependencies

RNN 固然有很多优点,但也会随之而来一些问题。

image-20230923170101778

Vanishing, Exploding Problem

比如上一篇文章我们提到了梯度消失与梯度爆炸 (Gradient Vanishing & Gradient Exploding)

image-20230918165941126

由于 RNN 的结构在求偏导时很容易出现类似上图这种情况,结果就是偏导的连乘中有乘数很大 (小),那么得到的结果就会很大 (小),导致梯度爆炸即loss突然变得很大 (梯度消失即loss一直不更新)。

解决的办法上一篇文章页详细说过,有

  • BN 结,即批标准化 (Batch Normalization):相当于一个放大器,将过小的损失进行放大
    image-20230918164852050
  • 梯度裁剪 (Gradient Clipping):直接将过大的梯度值变小

等等。

image-20230923170231414

LSTM 长短期记忆神经网络

LSTM 全称 Long Short Term Memory,直译过来就是长短期记忆,但根据我们之前的讲解很容易理解,它的能力其实是——既能处理长序列、又能处理短序列。

LSTM 的出现其实就是为了解决深度很深的模型容易出现梯度消失和梯度爆炸这一问题的。

这个问题是通过门(Gate)来解决的,这里的Gate更像是“阀门”:用于控制多少信息流到下一层的一种结构。

Gate: Information Control

  • Converge faster
  • Detect long-term dependencies
  • Gates are a way to optionally let information through. They are composed out of sigmoid neural net layer and a pointwise multiplication operation.

这是一般的 RNN 结构

image-20230923171749660

这是 LSTM 的结构

image-20230923171916905

LSTM 原理

我们来解释一下 LSTM 到底做了什么
f t = σ ( W f ⋅ [ h t − 1 , x t ] + b f ) i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) C t ~ = t a n h ( W c ⋅ [ h t − 1 , x t ] + b c ) C t = f t ∗ C t − 1 + i t ∗ C t ~ O t = σ ( W o [ h t − 1 , x t ] + b o ) h t = O t ∗ t a n h ( C t ) f_t = \sigma(W_f\cdot[h_{t-1}, x_t]+b_f)\\ i_t = \sigma(W_i\cdot[h_{t-1}, x_t]+b_i)\\ \widetilde{C_t} = tanh(W_c\cdot[h_{t-1}, x_t]+b_c)\\ C_t = f_t*C_{t-1}+i_t*\widetilde{C_t}\\ O_t = \sigma(W_o[h_{t-1}, x_t]+b_o)\\ h_t = O_t*tanh(C_t) ft=σ(Wf[ht1,xt]+bf)it=σ(Wi[ht1,xt]+bi)Ct =tanh(Wc[ht1,xt]+bc)Ct=ftCt1+itCt Ot=σ(Wo[ht1,xt]+bo)ht=Ottanh(Ct)

  • Step-01: Decide how much past data it should remember
    f t = σ ( W f ⋅ [ h t − 1 , x t ] + b f ) f_t = \sigma(W_f\cdot[h_{t-1}, x_t]+b_f) ft=σ(Wf[ht1,xt]+bf)
    这里的f_t指的是forget gate,决定前一步 (previous time step) 中删掉哪个不重要的信息。

    可以看到f_t是一个sigmoid函数的输出值,也就是说它的范围在0~1。sigmoid表示——留多少信息,其他 sigmoid同理

  • Step-02: Decide how much this unit adds to current state
    i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) C t ~ = t a n h ( W c ⋅ [ h t − 1 , x t ] + b c ) i_t = \sigma(W_i\cdot[h_{t-1}, x_t]+b_i)\\ \widetilde{C_t} = tanh(W_c\cdot[h_{t-1}, x_t]+b_c) it=σ(Wi[ht1,xt]+bi)Ct =tanh(Wc[ht1,xt]+bc)
    这里的i_t指的是input gate,根据信息的重要程度决定哪些信息在当前步 (current time step) 可以通过 (let through)。

    至此,我们就能看懂这个式子了
    C t = f t ∗ C t − 1 + i t ∗ C t ~ C_t = f_t*C_{t-1}+i_t*\widetilde{C_t} Ct=ftCt1+itCt
    其中,f_t的范围是01,`i_t`的范围是01,~C_t的范围是-1~1。之所以~C_t用tanh函数是因为tanh表示——对下一步的影响 (可能是负的),其他 tanh同理

    于是就有,

    现在的C_t = 上一次的C_{t-1} × 遗忘门 (决定删掉过去多少) + 这次计算出的~C_t × 输入门 (决定保留这次多少)

  • Step-03: Decide what part of the current cell state makes it to the output
    O t = σ ( W o [ h t − 1 , x t ] + b o ) h t = O t ∗ t a n h ( C t ) O_t = \sigma(W_o[h_{t-1}, x_t]+b_o)\\ h_t = O_t*tanh(C_t) Ot=σ(Wo[ht1,xt]+bo)ht=Ottanh(Ct)
    这里的O_t指的是output gate,允许那些通过的信息 (passed information) 去影响当前步 (current time step) 的输出。

我将各自的参数以及输出结果都在下图中标注出来了,对照一下公式应该还是很清晰的。

image-20230923221722434

所以回到最开始的问题,为什么 LSTM 能够解决梯度消失和梯度爆炸的问题?

这里可以从两方面进行解释:

  • 选择性过滤:LSTM 只保留了重要的数据,而过滤掉了那些不重要的,因此在求偏导的时候数据会更加“正常”。
  • 微观解释:在求导时我们涉及到求∂C_t/∂C_{t-1},也正是在这个过程中容易梯度爆炸或消失。但是在 LSTM 中会发现求导结果就是f_t (注意f_t在每步中是由W_f拟合得到的),假如C_{t-1}C_t影响很大,那么f_t就会接近1,反之f_t就会接近0。这样一来,遗忘门f_t的值就是偏导结果,影响大结果就是1,影响小结果就是0表示不更新,从而避免了在一连串的求偏导中部分极端值影响最终偏导结果的情况。

Peephole Connection

We let the gate layers look at the cell state

当然 LSTM 不止这一种结构,一种叫Peephole Connection的结构是这么设计的

image-20230923231243748

也就是额外将C_{t-1}C_t加入了三个gates的计算中 (*)
∗   f t = σ ( W f ⋅ [ C t − 1 , h t − 1 , x t ] + b f ) ∗   i t = σ ( W i ⋅ [ C t − 1 , h t − 1 , x t ] + b i ) C t ~ = t a n h ( W c ⋅ [ h t − 1 , x t ] + b c ) C t = f t ∗ C t − 1 + i t ∗ C t ~ ∗   O t = σ ( W o [ C t , h t − 1 , x t ] + b o ) h t = O t ∗ t a n h ( C t ) *\space f_t = \sigma(W_f\cdot[\boldsymbol{C_{t-1}}, h_{t-1}, x_t]+b_f)\\ *\space i_t = \sigma(W_i\cdot[\boldsymbol{C_{t-1}}, h_{t-1}, x_t]+b_i)\\ \widetilde{C_t} = tanh(W_c\cdot[h_{t-1}, x_t]+b_c)\\ C_t = f_t*C_{t-1}+i_t*\widetilde{C_t}\\ *\space O_t = \sigma(W_o[\boldsymbol{C_t}, h_{t-1}, x_t]+b_o)\\ h_t = O_t*tanh(C_t)  ft=σ(Wf[Ct1,ht1,xt]+bf) it=σ(Wi[Ct1,ht1,xt]+bi)Ct =tanh(Wc[ht1,xt]+bc)Ct=ftCt1+itCt  Ot=σ(Wo[Ct,ht1,xt]+bo)ht=Ottanh(Ct)

RNN的改进版:GRU

Simpler and fewer parameters

GRU 全称 Gated Recurrent Unit,它之所以能用到更少的参数是因为遗忘门f_t输入门i_t之间存在一定关系——遗忘门f_t越大表示对过去记忆保留越多,输入门i_t越大表示对当前结果保留越多,因此我们完全可以设置f_t = 1 - i_t

因此,GRU 的构造如下

image-20230923232638645

满足
r t = σ ( W r ⋅ [ h t − 1 , x t ] ) z t = σ ( W z ⋅ [ h t − 1 , x t ] ) h t ~ = t a n h ( W ⋅ [ r t ∗ h t − 1 , x t ] ) h t = ( 1 − z t ) ∗ h t − 1 + z t ∗ h t ~ r_t = \sigma(W_r\cdot[h_{t-1}, x_t])\\ z_t = \sigma(W_z\cdot[h_{t-1}, x_t])\\ \widetilde{h_t} = tanh(W\cdot[r_t*h_{t-1}, x_t])\\ h_t = (1-z_t)*h_{t-1}+z_t*\widetilde{h_t}\\ rt=σ(Wr[ht1,xt])zt=σ(Wz[ht1,xt])ht =tanh(W[rtht1,xt])ht=(1zt)ht1+ztht
其中,r_t对应是遗忘门,z_t对应的是输入门(当然在这里如何对应已经无所谓了)。

可以看到,GRU 减少了一个参数W (要知道这可是一个矩阵),模型参数减少的结果就是:

  • 参数少了 -> 所需要的数据就少了 -> 同样的数据下模型拟合得就快了 -> 下降的时候模型变简单了 -> 更不容易过拟合

也就是训练得又快又好。

RNN 模型的应用 RNN Applications

RNN 可以被应用在以下方面

  • 预测问题 Prediction Problems

  • 语言 (情感) 判断&文本生成 Language Modelling and Generating Text

    NLP

    image-20230923234518414

  • 机器翻译 Machine Translation

    image-20230923234543154

  • 语音识别 Speech Recognition

  • 基于图片生成描述 Generating Image Descriptions

    Language Caption (2015)

    image-20230923234315604

  • 视频标签 Video Tagging

  • 文本总结 Text Summarization

  • 客服中心对话分析 Call Center Analysis

  • 音乐生成 Music composition

  • 11
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值