RNN与LSTM

前因后果

最近在搞论文,做的是基于rPPG的情感识别。

其中用到的网络中,出现了我之前经常听室友说过,但我从来没怎么去了解的Encoder与Decoder。

我的第一反应是:难道不是解析HTTP的编码器与解码器?

并且这个网络里面,使用到了Encoder-Decoder的思想, 我看不懂,但我大受震撼。

为了弄清楚网络,我就只能去网上冲浪,看看有没有相关的博客或者视频,讲这个结构的。

那么回到开始,什么是rPPG?

rPPG简介

rPPG,全称remote photoplethysmograph,即远程光电容积脉搏波。

PPG信号全名叫做光电容积脉搏波,也是一种信号,类似于ECG信号,一般通过手指进行测量,通过对该信号进行处理也可以提取出心率值;其原理如下图,由于人的心脏跳动,导致血管中血容量发生变化,其对于光的反射与透射情况会发生改变,这时通过一个设备来接受反射的光线,可以测量出光线强度的变化,一次推导出脉搏。

PPG的原理

rPPG则是后来衍生出的名称,是通过远程测量的一种信号,即基于视频进行测量得到的信号,通过该信号可以计算出心率。由此可以看出,ECG,PPG,rPPG都是一种可以提取心率的信号,不同的是它们的测量方式不一样,需要的设备不一样。

所以简而言之,我的这个方向,仍然是通过计算机视觉来解决问题,这样可以实现非接触式的测量,还是很有发展前景的。

RNN

简介

提到Encoder与Decoder,就不得不先说说RNN。

RNN全称为Recurrent Neural Networks,翻译为循环神经网络。

人类在思考时,每一秒的思考,都不会是完全独立的。人每一时刻的思维与思考,或多或少都会和以前的信息相关联。

当你读一篇文章,或者一个句子的时候,对于当前词汇的理解,都是建立在对于之前词汇的理解之上的。

比如一个句子:我爱打篮球

当你读到 篮球 时,结合了上一个词 ,你会理解到这是一个组合词,表达的意思是 打篮球 这么一个动作。同时结合前面 以及 ,你会解读出 喜欢 打篮球 这个状态。

所以你在理解句子时,不会把一切都扔掉,重新开始思考,你的思考是有持续性的。

而传统的神经网络做不到这些,这是神经网络的一个缺点。

例如,假设你想对电影中每一点发生的事件进行分类,传统的神经网络很难通过其对电影中先前事件的推理,来推理后续事件。

结构

所以就有人提出了RNN的概念。下面是RNN的基本网络结构:

RNN基本结构

可以很清楚的看到,RNN中包含了循环。每一个 t 时间输入的 Xt ,其送入了 A 中,同时其还包括了 t-1 时刻 A 的输出。

将这个图平坦铺开,可能会更好理解一些:

铺开的RNN

这种链状性质表明,循环神经网络与序列和列表密切相关,这种结构决定了循环神经网络在处理序列型数据时,有天然的优势。

但是读者看到这个结构,应该也能想到一个问题,那就是我从最开始时刻送入的 X1 数据,会一直延续到 Xt ,也就是 Long-Term

Long-Term的缺点

RNN的一个优点是,它们可能能够将以前的信息连接到当前任务,例如,使用以前的视频帧可能有助于理解当前帧。如果RNN能够做到这一点,它们将非常有用。但RNN真的可以吗?不好说。

有的时候,我们在理解一个句子的时候,实际上只需要携带前面较少的信息,就可以很好地进行推理。

而上面RNN的结构,就会导致网络的记忆能力过于 强大 (虽然越早进入网络的输入 X ,在后续的 A 中占比可能会越来越小),我们在做推断的时候,可能根本不需要那么早的信息。甚至这些信息还会对网络的推理产生负面的影响。

但是也不能说,一定要把之前的信息全部丢掉。

理论上,RNN完全能够处理这种 Long-Term ,一个人可以仔细地为他们挑选参数来解决这种形式的玩具问题。遗憾的是,在实践中,RNN似乎无法学习它们。

既然都是人工智能深度学习了,那有没有一种网络,可以很 智能 地记忆呢?😏

LSTM

感谢先驱 Hochreiter & Schmidhuber 于1997(我还没出生)提出了LSTM,一个非常经典的、特殊的RNN,并在后续工作中得到了许多人的改进和推广。

经典LSTM

结构如下:

经典LSTM

经典的LSTM有三个门(Gate),分别为遗忘门(Forget Gate)、输入门(Input Gate)以及输出门(Output Gate)

遗忘门(Forget Gate)

遗忘门

遗忘门的公式为:

ft=σ(Wf[ht1,xt]+bf)f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)

这里将 h(t-1)Xt 拼接后,与 Wf 矩阵相乘,并且加上了 bf 偏执矩阵,最后通过 Sigmoid 函数来激活,最后输出 0 或者 1 ,该输出与 C(t-1) 相乘,决定了 C(t-1) 是否保留。

f(t) 由 t-1 时刻的 h(t-1) 以及 t 时刻的 Xt 计算出来,并且只有 0 和 1,分别代表了保留与遗忘,就像一个门电路一样,所以将其称为了 Forget Gate

输入门

输入门

输入门的公式为:

it=σ(Wi[ht1,xt]+bi)i_t = \sigma(W_i\cdot[h_{t-1},x_t]+b_i)

Ct~=tanh(Wc[ht1,xt]+bC)\widetilde{C_t}=tanh(W_c\cdot[h_{t-1},x_t]+b_C)

这里的 it 就是 Input Gate,决定了网络是否保存当前的输入

输出的Ct

最终的Ct公式为:

Ct=ftCt1+itCt~C_t=f_t*C_{t-1}+i_t*\widetilde{C_t}

输出门

输出门

输出门也很好理解了,就是决定网络是否要将当前计算出来的结果(其实也就是Ct)输出。其公式为:

ot=σ(Wo[ht1,xt]+bo)o_t=\sigma(W_o\cdot[h_{t-1},x_t]+b_o)

ht=ottanh(Ct)h_t=o_t*tanh(C_t)

总结

其实花里胡哨的一堆门,其核心思想就是通过当前时刻的输入 Xt 以及上一时刻输入的 h(t-1) 来计算:

  1. 是否要把上一时刻的输出保留,通俗点说就是是否要保存记忆
  2. 是否要把当前时刻的输入,加入到网络中,通俗点说就是记忆里是否要加入新的数据
  3. 是否要把当前时刻的结果,输出,通俗点就是是否要把记忆力的数据,输出出去

说了这么多,那么和encoder与decoder啥关系呢?

encoder与decoder的概念

其实encoder与decoder这样一个结构,只是个概念,或者说思想。

E-D大多数时候还是用于NLP相关的场景中,尤其是sequence-to-sequence,也就是输入序列,输出序列。

并且sequence2sequence也可以解决输入序列与输出序列在形状与大小上不同的问题。

比如一句话:我是中国人

在分词的时候,会将其分为

  • 中国

但是将其翻译后,应该为 I am chinese,其分词为:

  • I
  • am
  • chinese

可以很明显的看出来,输入的序列长度为4,输出的序列为3。

所以就使用encoder将输入序列编码(encode)为固定长度的中间信息,最后使用decoder将中间信息解码(decode)为输出。

encoder与decoder

论文中的encoder与decoder

我在复现 PhysNet 论文时,使用作者提供的代码。在阅读其代码的时候,我一直很迷惑作者在论文中提出的E-D,到底在哪里呢?

而且在看了RNN以及encoder-decoder的文章以及视频后,更加困惑着,网络结构根本没有这么复杂啊!

直到昨天,在闲暇之余,突然想到encoder-decoder只是一种思想,没有规定说一定要使用RNN以及LSTM。

我重新看了看代码,以及作者的一部分注释,我才明白了E-D思想是怎么体现的。

1
2
3
4
5
6
7
8
9
10
11
12
self.upsample = nn.Sequential(
nn.ConvTranspose3d(in_channels=64, out_channels=64, kernel_size=(4, 1, 1), stride=(2, 1, 1),
padding=(1, 0, 0)), # [1, 128, 32]
nn.BatchNorm3d(64),
nn.ELU(),
)
self.upsample2 = nn.Sequential(
nn.ConvTranspose3d(in_channels=64, out_channels=64, kernel_size=(4, 1, 1), stride=(2, 1, 1),
padding=(1, 0, 0)), # [1, 128, 32]
nn.BatchNorm3d(64),
nn.ELU(),
)

这里用到了 nn.ConvTranspose3d 这个转置卷积函数,简要来说,是将卷积后大小缩小的矩阵,通过转置卷积,将其恢复到卷积前的大小(虽然数据已经变了)

1
2
3
4
5
x = self.ConvBlock8(x)  # x [64, T/4, 8, 8]
x = self.ConvBlock9(x) # x [64, T/4, 8, 8]

x = self.upsample(x) # x [64, T/2, 8, 8]
x = self.upsample2(x) # x [64, T, 8, 8]

在上述代码中,x的形状经过了多层的卷积操作,大小变为了 [64, T/4, 8, 8],而最开始输入的大小为 [3, T, 128, 128],我们想要将其第二个维度的大小,从T/4变回T,就可以使用 self.upsample 中的转置卷积操作。

其encode与decode分别是:

  • encode:将原信号进行卷积以及时域最大池化,所以时间维度的大小从T不断地变为T/4
  • decode:为了将时间维度大小恢复,使用了两次转置卷积,将时间维度大小恢复为T

虽然有点抽象,但是也大概理解了E-D的思路。

至于为啥这个操作有用,我目前还没领悟出来,还得多做实验。

参考与鸣谢

  1. 李宏毅老师的RNN讲解视频:李宏毅机器学习-RNN网络(中英文)_哔哩哔哩_bilibili
  2. 写的非常好的一个博客,但是是生肉:Understanding LSTM Networks – colah’s blog
  3. 转置矩阵讲解1:转置卷积(Transpose Convolution) - 知乎 (zhihu.com)
  4. 转置矩阵讲解2:转置卷积(反卷积) - 知乎 (zhihu.com)

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!