乐趣区

经验拾忆纯手工-CNNRNNNg

前言

看 Andrew Ng 视频,总结的学习心得。
虽然本篇文章可能不是那么细致入微,甚至可能有了解偏差。
但是,我喜欢用更直白的方式去理解知识。
上一篇文章传送门:https://segmentfault.com/a/11…

端到端

首先聊一个面试经历

我最开始接触的是 ML(但只限于 Sklearn 的简单应用,工程化的内容当时一点都不了解。)
后来有幸了解到 DL(这个了解比较多)
我面的是普通 Python 岗,因为我的小项目中涉及到(聊天机器人)。所以第二个面试官揪着这个聊了聊。
与面试官交谈时,我也直接挑明了,模型是 Github 找的,当时自己爬了些问答对,处理后放入模型自己训练的。
面试官一顿(特征提取,语义)等各种 ML-NLP 工程化的过程,把我直接问懵了。。

怎么提取特征(问号脸,难道是 TF-IDF,分词之类的??)?????
我也不知道说啥,以仅有的能力,和他聊聊(LSTM、Embedding, Seq2Seq 的 Encoder-Vector-Decoder)。。

面试官说:“你说了这些,那你特征工程是怎么做的???”
我感觉已经没有任何反驳的能力了。。。接下来的事情,我不说,大家也应该清楚了。

反思

我回来后也反思过,做了什么特征工程??
我看视频中 也是,数据简单预处理下,然后分词,词频过滤,构建词典
然后,直接就是构建 NN 层(包括 Embedding 层)。

直到最后了解了 ” 端到端这个概念 ” 与 传统 ML 的区别。
才清楚,当时面试的场景是怎么个情况。。。

正式开篇端到端

传统 ML:原数据 -> 数据特征工程 (各种复杂的人工处理) —> 模型
端到端 DL:原数据 —————————————————–> 模型

端到端:(一步到位):

传统的 ML 做的中间层人工 "手动" 特征工程处理出来的特征。这些特征,端到端的 NN 都可能 "自动学习" 的到。这也可能是当时为什么面试官一直追问我 "特征如何处理" 的原因吧。也肯能他另有目的 QAQ...
或者我们真的不在一个频道上。。。但是交流的过程真的使我受益匪浅,有了更广阔的视野(3Q!)

强调一点:

虽然端到端 模型很便捷。但是需要大量的数据,才能训练出好的效果。

CNN(卷积神经网络)

构成

卷积层(激活函数)+ 池化层    + 全连接层
Convolution       + Pooling   + Dense

至于一些术语:

有人喜欢把: 卷积层 + 池化层   作为一层网络(因为池化层无训练训练,后面会提到)也有人喜欢把:  卷积层 和 池化层 各自单独算一个层(也是没问题的。Tensorflow 的 API 就是这样设计的)

卷积层(Convolution Layer)

卷积过程

卷积计算过程就不说了。没有案例图。但你可以理解为: 两个 正方体 的 对应位置的元素(相乘再相加)的结果。。。(互相关,,,)

卷积的输出计算

输出图像大小计算公式:

 h 图片输出 =(h 图片输入 - h 卷积核 + 2padding)/ strides + 1
w 图片输出 =(w 图片输入 - w 卷积核 + 2padding)/ strides + 1

首先声明: 这个式子由于不一定能够整除,因此除不尽的情况下,向下取整, 也叫地板除
因为有个原则: 卷积核滑动的时候(通常是,步长 >1 的情况下) 如果越界了一部分。则舍弃掉

根据上面的公式,求一个输出图像大小的例子(此处,不做 paddding,并且步长为 1)

eg: 输入 8x8 的图像 ,  并使用 3x3 的卷积核  
输出图像高度为:  h 图片输出 = (8-3 + 2x0) / 1 + 1 = 6
输出图像宽度为:  w 图片输出 = (8-3 + 2x0) / 1 + 1 = 6
所以输出图像为:  6x6

很明显: 卷积一次,图像变小了。如果再卷积几次,图像就看不到了。。。所以: 我们需要解决这个问题
原则上: 增加 padding 能解决步长为 1 时,卷积后的图片缩放问题。

假如我们希望输出图像的大小 等于 输出图像的大小,而我们想要求 padding 需要设置为多少。
一般这种场景适用于 步长 strides = 1, 所以参考开始的公式,可写出如下公式:

因为: w 和 h 是一样的公式,此处只写出一个 h, 来推导:h 图片输出 =(h 图片输入 - h 卷积核 + 2padding)/ strides + 1
化简:
    padding =(h 图片输出 - h 图片输入 + h 卷积核 - 1)/ 2
因为我们希望的条件: h 图片输出 等于 h 图片输入,所以可继续化简:padding =(h 卷积核 - 1)/ 2    

所以步长为 1 的情况下卷积, 并且想让图片不变形,你的 padding 的取值,就需要受到 卷积核大小的影响。
现在常用的卷积核大多都是 1×1、3×3、5×3。所以看上面化简好的公式:

padding =(h 卷积核 - 1)/ 2     <=============   1x1, 3x3, 5x5
奇数 - 1 总等于偶数。所以不用担心除不尽的情况
还需要注意一下: 填充 padding 一般是环形填充,假如 padding=1, 那么上下左右 都会添加一层。当然: tensorflow 的 padding 是可以设置形状的

padding 的种类(tensorflow)

valid:

不填充

same:

自动去填充,使得输入图像 与 输出图像 的大小相等

温馨提示:关于三通道卷积 和 多个卷积核的区别

三通道卷积:

假如你只有一个卷积核
即使你图片是 3 通道的(三层)即使你卷积核也是三通道的(三层)但是: 卷积输出结果是依然是一个  m x n 的形状  (一层 "薄纸") 
你的疑惑: 不是三层嘛?最后怎么变成一层了???我的解释: 每滑动一次,3 层通道,各自都会计算各自层的卷积,然后求总和,并填入一层 "薄纸" 的对应位置

多个卷积核:

上面说了: 1 个卷积核,无论图片是什么形状的、有几个通道,卷积后输出的形状 是 一层薄纸: m x n

而如果你有多个卷积核: 那么你就会有多层薄纸摞起来。就像 一个长方形 摞成了一个 长方体
明确点:  假如你用了 6 个卷积核, 那么你的输出就变成了   m x n x 6      (三维的一个长方体了)

上面说的:就是我最开始学的 CNN 时候经常理解不清楚的地方。我相信你也一样, qaq ….
下面做个总结:

 C 个通道:  一点都不会影响输出维度。注意我说的是维度。假如你的输入是 m x n , 那么你的输出依然是  p x q(注意维度即可,维度没变,二维)

f 个卷积核: 会影响输出的维度。输出的维度将会增加一个维度 f
    假如你的输入是 m x n,那么你的输出依然是  p x q x f  (增加一个维度 F,变成了)

也许你当你学 TF 的时候会有另一个理解障碍: 那就是 TF 数据格式(以图片为例):通常 TF 数据格式是这样的:[图片数量 B,  图片高度 H,  图片宽度 W,  通道数 C]
    假如你使用 F 个卷积核做了卷积:那么他的卷积结果的特征的形状就变变成:[B, H, W, F]
    发现没输出结果和通道数 C,没有关系的。只和 卷积核的个数 f 有关系。但是注意: 虽然结果和 C 没关系。但是 需要卷积核中具有 C 的数量,还做唯独匹配。桥梁运算。对应上例,我们的卷积核的形状应该是这样的 :   [F, C, H, W]
    注意一下:这里面有 卷积核数量 f,也有通道数量 C。

如果最后一步的卷积核形状不理解:

没关系。以后是 TF20 的天下。对应 API 不需要你指定卷积核的形状。因此,你没必要记住卷积核的形状。你只需要 传递,卷积核的个数,和 宽高 和 步长 即可。当然这些都是独立的命名参数。摘一小段 Conv2D 的源码:
    def __init__(self,
           filters,                # 你只需要传递这个参数,卷积核的个数
           kernel_size,            # 卷积核的宽高,假如 3x3 你只需写  [3,3] 即可
           strides=(1, 1),         # 这是步长,你不传,他也会给你填默认值, 步长为 1
           padding='valid',        # 这时 padding 策略,前面说过,这个一般都会设为 "same"
           
或许你还有些疑问:
    刚才上面不是提到了卷积核应该设置 通道数 C 么。原则上是的。因为要和 输出的样本做卷积。要匹配才行。但是在 Tensorflow 中。特别是 Tenosrflow.Keras 中,定义模型层
    我们只需要把整个模型,从上到下连接起来。(就像先后排队一样)而对于一些前后流动贯通的参数,比如刚才提到的通道 C。这些参数,Tensorflow 会自动帮我们上下文识别管理。所以我们做的只是需要,把原始数据的形状设置好传 给第一层(给排头发数据)至于你这些在中间层流动的参数,Tensorflow 会自动帮你计算,你不用传。虽然不用传,但你最好清楚每层是什么结构(当然这时后话,可能需要一些时间理解)到最后,我再给你设置一个输出形状,你能给我输出出来即可(队尾接数据)基本 TF 参数流动机制讲到这里,刚开始学的时候,也是各种苦思冥想,想不明白 qaq...

透过现象看本质(卷积 => 线性)

其实我们做的每一步 (每一个)卷积就相当于一个矩阵线性操作:x1 @ w1
之后,基于常理话,我会还会给它一个偏差:b 变成 ===> x1 @ w1 + b

我们说过,可能会给出很多个卷积核进行运算。

上面  x1 @ w1 + b    是每一个卷积核的卷积结果
我们还需要讲所有卷积核计算结果堆叠在一起:记为  X @ W + b     # m x n x f
最后将堆叠在一起的结果,做一层非线性变换 relu (X @ W + b)    # CNN 通常用 relu

eg:现有图片 5 x 5 x 3 的图像(暂时不考虑样本个数,就认为是一个样本).
     我们用的是 2 x 2 x  20 的卷积核 (步长为 1,不做 padding)
     那么输出结果就是   (5-2+1) x (5-2+1) x 20  ===  4 x 4 x 20

忘记说了,还有一个公式,用来计算 每层卷积的权重参数量的个数的:

公式:  每层权重参数量(W) = 卷积核个数 x 卷积核高 x 卷积核宽 x 卷积核通道数
公式:  每层偏差数量(b) = 1 x 卷积核的个数        # 因为每个卷积核只有一个偏差 b

温馨提示: 有太多人喜欢把卷积核个数 与 卷积核通道称作:"输入 / 输出" 通道。这样的称呼是没问题的,但我在计算参数量的时候,不喜欢这样的称呼,易混淆。前情回顾: 记不记得普通神经网络了。每个神经元节点,都有它们自己的参数。因此它们的参数量是巨大的
回归正文: 而卷积核是共享的,因为它是在一张图片上滑动的。(挨个服务)所以权重参数也是共享的。

池化层 (Pooling Layer)

卷积层 (激活函数) => 池化层
池化层主要分两种:MaxPooling 和 AvgPooling

池化层输出图片形状计算公式:

声明:池化层也有滑动窗口,并且输出形状计算公式,和 卷积的输出形状计算公式一样:

 h 图片输出 =(h 图片输入 - h 卷积核 + 2padding)/ strides + 1
w 图片输出 =(w 图片输入 - w 卷积核 + 2padding)/ strides + 1

因为池化层,的基本都是放在卷积层之后,因此池化层的通道数 也就顺理成章的 和 卷积层通道一样

举个例子:
卷积层数据形状为:  m x n x f
那么池化层形状同为: p x q x f

我想主要强调的是: 通道数不变,变得是 宽高。

池化层 滑动窗口参数相关配置

还是,把 Tensorflow, 源码搬过来,标注一下:

  def __init__(self,
           pool_size=(2, 2),   # 滑动窗口大小 2x2
           strides=None,       # 步长,通常设为 2 
           padding='valid',    # Maxpooling 通常不用 padding

一般都是使用组合 pool_size=(2, 2) 和 stride = 2


所以,公式来了:
                                                输入 h         滑动窗口 h
    输出 h = (输入 h - 滑动窗口 h) / stride + 1 = ----------  -   --------  + 1
                                                stride         stride

通常我们把 pooling 层作称作数据的降采样:

所以大多数经验者,都会把 滑动窗口 和 stride 步长  设为相等大小。所以带入上面公式:输入 h           1            输入 h
输出 h = (输入 h - 滑动窗口 h) / stride + 1 = ----------  -   -----  + 1 =  -------
                                            stride          1             步长
                                            
简化一下:(当 pool_size 和  strides 设置相等大小时):输出 = 输入 / 步长

    所以当我们: 
        步长设为 2 时,输出就是输出的一半。步长设为 3 时,输出就是输出的 1 /3。...

不知道有没有这样一个疑问:”为什么滑动窗口没有设置 窗口数量(就像设置卷积核数量)“

再次说一下 Tensorflow 的原理。因为 Pooling 的上一层基本完全是 Conv 卷积层,卷积层的 卷积核的个数已经设置好了。卷积层对接池化层的时候,Tensorflow 会自动判断,并设置:

池化层滑动窗口的个数 === 卷积核个数
池化层通道个数的个数 === 卷积层通道个数 === 图片的原始通道个数

MaxPooling(最大池化,常用)

卷积操作:之前我们卷积不是拿着滑动窗口,对应元素相乘再相加么?
池化操作:池化层也是拿着滑动窗口一样滑,但是不做运算,而是只取每个窗口内最大值。放在一层 ” 薄纸 ” 上

AvgPooling(平均池化,不常用)

一样滑动窗口,各种滑,然后取每个窗口内的数据的 "平均值",  其他就不说了,同 MaxPooling

额外提醒(池化层的参数是否训练)

池化层的是 "没有" 参数可以训练的。所以,反向传播,也不为所动~~~

全连接层(Dense Layer)

什么是全连接层??

你很熟悉的,全连接层其实就是之前讲的普通的 NN(神经网络),所以并没有什么好说的。
只是拼接在池化层之后罢了。
但其实还是有一些细节需要注意。尤其之前的东西没搞懂,那么这里的参数形状你会垮掉~~~

展平 及 参数

之前为了图方便,参数我都没怎么提到样本参数。
下面我要把样本参数也加进来一起唠唠了。我感觉讲这里,直接上例子比较直观。
好了,现在我们有个需求,想要做一个 10 分类的任务:

卷积层 - 池化层: 这个照常做,设置你还可以堆叠
    卷积层 1 + 池化层 1 + 卷积层 2 + 池化层 2 ...
等堆叠的差不多了: (你自我感觉良好了。。。),我们需要做一层展平处理!

展平处理(特意拿出来说)

假如你叠加到最后一层池化层数据形状是:(1000,4,4,8)==> 1000 个样本,宽高 8 x 8, 输出通道为 8 
你展平后的形状为: (1000, 4*4*8) == (1000, 128)  
    展平操作第一种 API:  tf.keras.Flatten()     # tensorflow2.0 的 Flatten 被作为网络层使用
    展平操作第一种 API:  tf.reshape(x, [-1, 128])  # 手动变换,- 1 是补位计算的意思
然后在加全连接层,形状为: (1000, 50)        # 50 代表输出,起到过渡作用
然后在加全连接层,形状为: (1000, 10)        # 最终,10 代表输出,因为我们说了,要做 10 分类嘛
    1. 其实你中间可以加很多全连接层,我这里只加了一层,控制最后一层全连接是你想要的输出就行。2. 特别注意,这里的每一层全连接计算,是需要有激活函数跟着的。除了最后一层全连接,其他层的全连接都设置为 Relu 激活函数即可。3. 因为我们做的是 10 分类(多分类自然应想到 softmax 参数,如果是其他业务,你也可以不加激活函数)没做,也就是最后一层。我们要再添加一层激活函数 Softmax。

1 x 1 卷积的作用

降采样(控制输出通道数量):

假如, 前一个卷积层参数为: (1000,32,32,256)
如果你下一层使用 1x1x128 的卷积,则对应参数为: (1000,32,32,128)  # 256 通道变成了 128 通道

CNN 文本分类(也许你看完下面的 RNN 再回来看这个会更好)

通常 CNN 大多数都是用来做 CV 工作。对于某些文本分类。CNN 也可以完成。如下变通概念:

  1. 句子的长度 看作 (图片的高度)
  2. embedding 的维度,看作 (图片的宽度)
  3. 卷积核是铺满一行 (或者多行),然后沿着高度竖着滑下来的。你也可以有多个卷积核
    eg: 一个句子 10 个词语,20dim,这个句子的输入形状就是(10 x 20)
    我们准备 3 个卷积核分别是(3×20),(2×20), (1×20)
    每个卷积核竖着滑下来,最后按次序得到向量形状为(10,3)
    你可以看作输出三通道(对应卷积核个数,这和之前讲的 CNN 原理一模一样)
    最终提取出来这个(10,3)是,一个句子 3 个通道的特征信息。
  4. 将 10×3 特征矩阵,通过 maxpooling 压缩成(1×3)的特征矩阵
  5. 放入 Dense 层,构建多输出单元的 n 分类模型。

ResNet (残差网络 Residual Networks)

问题引入:

是否网络层数越多越好,虽然堆叠更多的网络,可以使得参数丰富,并且可以学到更多更好的特征。
但是实际效果并非如此,而是出现,过拟合等现象。

ResNet 作者 何凯明:有感而发:按理说模型是应该越丰富越好的。可是出现了过拟合这种现象。
最少,更深层的网络的效果,应该比浅层网络的效果好吧。不至于差了那么多。
因此,他将此问题转换为一个深度模型优化的问题。

ResNet 相关配置

  1. batch-size: 256
  2. optimizer: SGD+Momentum(0.9)
  3. learning_rate: 初始化为 0.1,验证集出现梯度不下降的情况下,learning_rate 每次除以 10 衰减
  4. 每一层卷积层之后,都做 Batch Normalization
  5. 不使用 Dropout(其实应该是用了 BN, 所以就没有 Dropout)

RNN (循环神经网络)

直接引用 Andrew Ng 的降解图


可以看到,上图中有一些输入和输出:慢慢捋清。

  1. 第一个输入 x<1> 代表(你分词后的每一个句子中的第一个单词)x<2> 就是第二个单词喽
  2. 第二个输入 a<0> 代表 初始输入,(一般初始化为 0 矩阵)
  3. 前面两个输入,各会乘上各自的 权重矩阵,然后求和 得出 a<1> (这是临时输出)
  4. a<1> 乘上 一个权重参数 得到输出一: y<1>(这是终极输出一)(这就是图中黑框顶部的输出分支)
  5. a<1> 乘上 又一个新权重参数后,再加上 x<2> 乘以自己的权重参数得到 a<2>
  6. …… 你会发现 1- 5 步是个循环的过程,到第 5 步,a<1> 就相当于 最开始 a <0> 的地位,作为下一层的输入
  7. 题外话。其实每层的输出 y1 会替代下一层的 x 作为下一层的输入。(我会放到下面 ” 防坑解释 ” 中说)

然后将上述途中最后 2 行的公式化简,可得到如下形式:

防坑解释 (RNN 语言模型)

如果你看过了上面的图,你会很清楚,有多少个 x, 就会输出多少个 y。
上面第 7 点说过 : “ 其实每层的输出 y1 会替代下一层的 x 作为下一层的输入 ”, 该如何理解这句话???

假如你有这样一段文本: "我精通各种语言"   => 分词后的结果会变成 "我","精通","各种","语言"
一般的问答对这种的句子,处理流程是:(这里只先说一个):那就是:在句子的末尾添加一个 <END> 标识符
所以句子变成了:  "我","精通","各种","语言", "END"
这些单词都会预先转为(One-Hot 编码 或者 Embedding 编码)x1(初始值 0) => y1(我)       y1 有一定概率输出 "我",下面所有的 y 同理,只是概率性。x2(y1) => y2(精通)          如此每一层嵌套下来,相当于条件概率  P(精通 | 我)
x3(y2) => y3(各种)          P(各种 |(我,精通))
x4(y3) => y4(语言)          ...
x5(y4) => y5(<END>)         ...

不知道看了上例,你会不会有下面一连串的问号脸??:

  1. 为什么 y1-y5 输出都是精准的文字?
    答:我只是方便书写表示,其实每个输出的 Y 都是一个从词典选拔出来的词的概率。(多分类)
  2. 不是说 x1-x5 每个 x 应该输入固定句子的每个单词么???为什么变成了输入 y1-y5
    答:的确是这样的,但是我们的 y1-y5 都是朝着预测 x1-x5 的方向前进的。(这也是我们要训练的目标)

    所以: 可以近似把 y1-y5 等价于 x1-x5。所以用 y1-y5 替代了 x1-x5
    这样: 也可以把前后单词串联起来,让整个模型具有很强的关联性。比如: 你第一个 y1 就预测错了。那么之后的 y 很可能都预测错。(我的例子是:双色球概率)但是: 假如我们预测对了。那说明我们的模型的效果,已经特别好了(双色球每个球都预测对了~)
  3. 那我们就靠这 x 和 y 就能把前后语义都关联起来吗???
    答:当然不仅于此。你别忘了我们还有贯穿横向的输入啊,如最开始 RNN 图的 a<0>. a<1> 这些
  4. 既然你说 y 是从词典选拔出来的词的概率属性。那么这个概率怎么算?
    答:这问得太好了~~~

    前面说了: 一般都会预先给数据做 One-Hot 或 Embedding 编码。所以数据格式为: [0,0,....,1,...]   # 只有一个为 1
    基本上我们最后给输出都会套一层: softmax 激活函数,softmax 应该知道吧:e^x /(e^x1+..+e^x)
    所以: softmax 结果就是一个 和 One-Hot 形状一样的概率列表集合: [....., 最高概率,...]
    softmax 的结果(概率列表):(代表着预测 在词典中每一个单词的可能性)
  5. 那么损失函数怎么算呢??
    答:没错,损失函数也是我们最关注的。

    前面: 我们已经求出了 softmax 对应的结果列表 (...., 最高概率,...)
    损失函数: 我们使用的是交叉熵。交叉熵知道吧:  -(Σp*logq)# p 为真实值 One-hot, q 为预测值
    简单举个例子: 
        假如 softmax 预测结果列表为 :[0.01,0.35, 0.09, 0.55]  # 温馨提示,softmax 和为 1
        你的真实标签 One-Hot 列表为:  [0,   0,    0,    1]
        那么交叉熵损失就等于: -(0*log0.01 + 0*log0.35 + 0*log0.09 + 1*log0.55) = ...
        
    到此为止,我们第一层 NN 的输出的损失函数就已经计算完毕了。而我们训练整个网络需要计算整体的损失函数。所以,我们需要把上面的交叉熵损失求和,优化损失。

梯度爆炸 & 梯度消失

RNN 的梯度是累乘,所以 NN 层如果很多,可能会达到 指数级的梯度。

你应该听过一个小关于指数的小案例吧~~ (学如逆水行舟,不进则退~)
>>> 1.01 ** 365
37.78343433288728        # 每天进步 0.01,一年可以进步这些(对应梯度爆炸)>>> 0.99 ** 365
0.025517964452291125     # 每天退步 0.01,一年可以沦落至此(对应梯度消失)

梯度爆炸:

就是上面例子的原理。就不多说了。解决方式:梯度裁剪

梯度消失:

同上例,不好解决(于是 LSTM 网络出现,和 LSTM)

Tensorflow2.0(Stable)API

import tensorflow.keras as tk     # 注意我用的是 TF20 标准版,所以这样导入

tk.layers.SimpleRNN(
    units= 单元层,            # units 单元数,对应着你每个单词的个数
    return_sequences=False   # 默认值就是 False     
)

GRU

GRU 比 RNN 的每一层的多了一个 记忆信息 (相当于 RNN 的 h),这个记忆信息就像传送带一样,一直流通各层 RNN
然后还多了 2 个门 (r 门和 U 门),这 2 个门就是负责控制(是否从传送带上取记忆,且取多少记忆)

注明:GRU 只有一个 c(横向,传送带),没有 h

简化版(只有 U 门):

 C 新 ' = tanh(w @ [C 旧, x 新] + b )   # 根据传动带的旧信息,生产出 传送带的新信息
u 门 = sigmoid (w @ [c 旧, x 新] + b)     # 一个门控单元,起到过滤信息的作用

C 新 = u 门 * C 新 ' + (1- u 门) * C 旧    #  经过 u 门控单元的控制过滤后,最终放到传送带的信息
如果: u 门为 1,则传送带上全是新信息(旧的全忘掉)如果: u 门为 0,则传送带上全是旧信息(新的不要)强调一下: 我不方便写公式负号,于是用了 "新","旧" 代替
新: 代表当前 t
旧: 代表前一时刻 t-1

完整版(同时具有 r 门和 u 门)
添加这一行:

 r 门 = sigmoid (w @ [c 旧, x 新] + b)     # 和下面的 U 门几乎相似,只不过换了一下权重和偏差
C 新 ' = tanh(w @ [r 门 @ C 旧, x 新] + b )   # 修改这一行: C 旧 == 变为 ===>  r 门 @ C 旧

u 门 = sigmoid (w @ [c 旧, x 新] + b)     # 一个门控单元,起到过滤信息的作用
C 新 = u 门 * C 新 ' + (1- u 门) * C 旧    #  经过 u 门控单元的控制过滤后,最终放到传送带的信息

Tensorflow2.0(Stable)API

import tensorflow.keras as tk     # 注意我用的是 TF20 标准版,所以这样导入

tk.layers.GRU(                    # 参数同上面 RNN 我就不解释了
    units=64,            
    return_sequences=False       # 这些参数看下面 LSTM 我会讲到
)

LSTM

LSTM 和 GRU 很像,但是比 GRU 复杂。
LSTM 结构包括: u 门(更新门)+ f 门(遗忘门)+ o 门(输出门)

注明:LSTM 不仅有个传送带 C(横向),他还有个 RNN 的 h 信息(横向)

 f 门 = sigmoid (w @ [c 旧, x 新] + b)     # 和下面的 U 门几乎相似,只不过换了一下权重和偏差
o 门 = sigmoid (w @ [c 旧, x 新] + b)     # 和下面的 U 门几乎相似,只不过换了一下权重和偏差

C 新 ' = tanh(w @ [C 旧, x 新] + b )      # 注意,这里没有 r 门了

u 门 = sigmoid (w @ [c 旧, x 新] + b)     # 一个门控单元,起到过滤信息的作用
C 新 = u 门 * C 新 '+ f 门 * C 旧        #"(1- u 门)"  换成了 f 门
h = o 门 * tanh(C 新)

Tensorflow2.0(Stable)API

import tensorflow.keras as tk     # 注意我用的是 TF20 标准版,所以这样导入

keras.layers.LSTM(
    units=64,
    return_state=True                 # 占坑,下面剖析
    return_sequences=False            # 占坑,下面源码剖析
    recurrent_initializer='glorot_uniform',   # 均匀分布的权重参数初始化

    # stateful=True, # API 文档:若为 True, 则每一批样本的 state 的状态,都会继续赋予下一批样本
)

return_state 和 return_sequences 这两个参数到底有什么用???
我的另一篇文章单独源码分析这两个参数:https://segmentfault.com/a/11…

总结对比 GRU 和 LSTM

GRU 有 2 个门:   u 门 和 r 门
LSTM 有 3 个门:  u 门 和 f 门 和 o 门

GRU 有一个 C:          # 就有一条传送带 c, 他的前后单元信息仅靠这一条传送带来沟通(舍弃与保留)LSTM 有一个 C 和一个 h:  # 不仅有传送带 c, 还有 h,他的前后单元信息 靠 c 和 h 联合沟通。再说一下每个门控单元: 不管你是什么门,都是由 Sigmoid() 包裹着的。所以: 说是 0 和 1,但严格意义上,只是无穷接近。但是微乎其微,所以我们理解为近似相等 0 和 1 

RNN-LSTM-GRU 拓展

双向(Bidirection)

首先说明:

双向模型,对于 RNN/LSTM/GRU  全部都适用

由于单向的模型,不能关联之后信息。比如:你只能根据之前的单词预测下一个单词。
而双向的模型,可以根据前后上下文的语境,来预测当前的下一个单词。
或者举一个更直白的例子(我自己认为):

比如说: 你做英语完型填空题,你是不是 需要 把空缺部分的 前面 和 后面 都得读一遍,才能填上。

单向与双向结构对比如下:

单向: 1 -> 2 -> 3 -> 4
双向: 1 -> 2 -> 3 -> 4 
                     |
      1 <- 2 <- 3 <- 4

注意: 上下对齐,代表一层。

Tensorflow2.0(Stable)API

import tensorflow.keras as tk     # 注意我用的是 TF20 标准版,所以这样导入

tk.layers.Bidirectional(        # 就在上上面那些 API 的基础上, 外面嵌套一个 这个层即可。tk.layers.GRU(              
        units=64,            
        return_sequences=False
    )
),

模型深层堆叠(纵向堆叠)

首先说明:

层叠模型对于 RNN/LSTM/GRU  同样全部都适用

之前单层单向模型是这种结构

1 -> 2 -> 3
计算公式是:  单元 = tanh (W @ (x, h 左) )

而多层单向是这种结构(我们以 2 层为例):

y1   y2   y3        输出层
^    ^    ^
|    |    |
7 -> 8 -> 9         二层单元
^    ^    ^
|    |    |
4 -> 5 -> 6         一层单元
^    ^    ^
|    |    |
x1-> x2 ->x3        输入层
你   好   啊

计算公式是:(我写的可能只按自己的意思了~)一层每个单元 = tanh (W @ (x, h 左) )     # 因为是第一层嘛:所以输入为 x 和 左边单元 h 
    二层每个单元 = tanh (W @ (h 下, h 左) )   # 第二层就没有 x 了: 而是下边单元 h 和 左边单元 h 

词嵌入(Word Embedding)

单词之间相似度计算

                       c1 @ c2
余弦定理,求 cosθ = ------------------
                     ||c1|| * ||c2||
           
或者你可以使用欧氏距离。

原始词嵌入并训练

  1. 假如我们通过一个句子的一部分来预测,这个句子的最后一个单词。
  2. 把词典的每个词做成 One-Hot 便是形式,记作矩阵 O
  3. 随机高维权重矩阵,记为 E
  4. E @ O 矩阵乘积后记为词向量 W
    可见如下案例:

    如果:  我们分词后词典总大小为 1000
    那么:  他的 One-Hot 矩阵形状为  [6, 1000](假如我们这里通过句子 6 个词来预测最后一个词)并且:  随机高维权重矩阵 形状为 [1000, 300](注意,这个 300 是维度,可自行调整选择)注意:  上面权重矩阵是随机初始化的,后面训练调节的。最后:  E @ O 后得到词向量 W 的形状为  [6, 300]
  5. 送进 NN(打成 1000 类) 作为输出
  6. 加一层 Softmax(算出 1000 个单词的概率)作为最终输出 y_predict
  7. y_predict 与 y 真正的单词标签(one-hot 后的)做交叉熵 loss
  8. 优化 loss,开始训练。

Word2Vec 的 skip-grams(不是太懂,Pass)

说下个人的理解,可能不对
skip-grams:拿出中间 一个词,来预测若干(这是词距,自己给定)上下文的单词。
例子如下:

seq = 今天去吃饭

给定单词   标签值(y_true)去         今
去         天
去         吃
去         饭

训练过程就是上面说过的小节 “ 原始词嵌入并训练 ”,你只需把 y_true 改为 “ 今 ”,” 天 ”,” 吃 ”,” 饭 ” 训练即可。
Word2Vec 除了 skip-grams, 还有 CBOW 模型。它的作用是 给定上下文,来预测中间的词。
据说效率等某种原因(softmax 计算慢,因为分母巨大),这两个都没看。(Pass~)

负采样(Negative Sampling)

解决 Word2Vec 的 softmax 计算慢。
负采样说明(假如我们有 1000 长度的词典):

从上下文(指定词距):   随机,选择一个正样本对,n 个负样本对(5-10 个即可)主要机制: 将 Word2Vec 的 softmax(1000 分类)换成 1000 个 sigmoid 做二分类。因为: 是随机采样(假设,采样 1 个正样本 和 5 个负样本)。所以: 1000 个 sigmoid 二分类器,每次只用到 6 个对应分类器(1 个正样本分类器,5 个负样本分类器)

负采样,样本随机选择公式:

单个词频 ^ (3/4)    
-----------------
Σ(所有词频 ^ (3/4))
退出移动版