AI => CNN-RNN(Ng)

前言

看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的取值,就需要受到 卷积核大小的影响。
现在常用的卷积核大多都是 1x1、 3x3、 5x3 。所以看上面化简好的公式:

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个卷积核分别是(3x20),(2x20), (1x20)
    每个卷积核竖着滑下来,最后按次序得到向量形状为(10,3)
    你可以看作输出三通道(对应卷积核个数,这和之前讲的CNN原理一模一样)
    最终提取出来这个(10,3)是,一个句子3个通道的特征信息。
  4. 将10x3特征矩阵,通过maxpooling压缩成 (1x3)的特征矩阵
  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的降解图

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

  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行的公式化简,可得到如下形式:

clipboard.png

防坑解释 (RNN语言模型)

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

假如你有这样一段文本: "我精通各种语言"   => 分词后的结果会变成 "我","精通","各种","语言"
一般的问答对这种的句子,处理流程是: (这里只先说一个):
那就是:在句子的末尾添加一个  标识符
所以句子变成了:  "我","精通","各种","语言", "END"
这些单词都会预先转为 (One-Hot编码 或者Embedding编码)

x1(初始值0) => y1(我)       y1有一定概率输出 "我", 下面所有的y同理,只是概率性。
x2(y1) => y2(精通)          如此每一层嵌套下来,相当于条件概率  P(精通|我)
x3(y2) => y3(各种)          P(各种|(我,精通))
x4(y3) => y4(语言)          ...
x5(y4) => y5()         ...

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

  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))

你可能感兴趣的:(cnn,deeplearning,python)