共计 12193 个字符,预计需要花费 31 分钟才能阅读完成。
前言
看 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 也可以完成。如下变通概念:
- 句子的长度 看作 (图片的高度)
- embedding 的维度,看作 (图片的宽度)
- 卷积核是铺满一行 (或者多行),然后沿着高度竖着滑下来的。你也可以有多个卷积核
eg: 一个句子 10 个词语,20dim,这个句子的输入形状就是(10 x 20)
我们准备 3 个卷积核分别是(3×20),(2×20), (1×20)
每个卷积核竖着滑下来,最后按次序得到向量形状为(10,3)
你可以看作输出三通道(对应卷积核个数,这和之前讲的 CNN 原理一模一样)
最终提取出来这个(10,3)是,一个句子 3 个通道的特征信息。 - 将 10×3 特征矩阵,通过 maxpooling 压缩成(1×3)的特征矩阵
- 放入 Dense 层,构建多输出单元的 n 分类模型。
ResNet (残差网络 Residual Networks)
问题引入:
是否网络层数越多越好,虽然堆叠更多的网络,可以使得参数丰富,并且可以学到更多更好的特征。
但是实际效果并非如此,而是出现,过拟合等现象。
ResNet 作者 何凯明:有感而发:按理说模型是应该越丰富越好的。可是出现了过拟合这种现象。
最少,更深层的网络的效果,应该比浅层网络的效果好吧。不至于差了那么多。
因此,他将此问题转换为一个深度模型优化的问题。
ResNet 相关配置
- batch-size: 256
- optimizer: SGD+Momentum(0.9)
- learning_rate: 初始化为 0.1,验证集出现梯度不下降的情况下,learning_rate 每次除以 10 衰减
- 每一层卷积层之后,都做 Batch Normalization
- 不使用 Dropout(其实应该是用了 BN, 所以就没有 Dropout)
RNN (循环神经网络)
直接引用 Andrew Ng 的降解图
可以看到,上图中有一些输入和输出:慢慢捋清。
- 第一个输入 x<1> 代表(你分词后的每一个句子中的第一个单词)x<2> 就是第二个单词喽
- 第二个输入 a<0> 代表 初始输入,(一般初始化为 0 矩阵)
- 前面两个输入,各会乘上各自的 权重矩阵,然后求和 得出 a<1> (这是临时输出)
- a<1> 乘上 一个权重参数 得到输出一: y<1>(这是终极输出一)(这就是图中黑框顶部的输出分支)
- a<1> 乘上 又一个新权重参数后,再加上 x<2> 乘以自己的权重参数得到 a<2>
- …… 你会发现 1- 5 步是个循环的过程,到第 5 步,a<1> 就相当于 最开始 a <0> 的地位,作为下一层的输入
- 题外话。其实每层的输出 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>) ...
不知道看了上例,你会不会有下面一连串的问号脸??:
- 为什么 y1-y5 输出都是精准的文字?
答:我只是方便书写表示,其实每个输出的 Y 都是一个从词典选拔出来的词的概率。(多分类) -
不是说 x1-x5 每个 x 应该输入固定句子的每个单词么???为什么变成了输入 y1-y5
答:的确是这样的,但是我们的 y1-y5 都是朝着预测 x1-x5 的方向前进的。(这也是我们要训练的目标)所以: 可以近似把 y1-y5 等价于 x1-x5。所以用 y1-y5 替代了 x1-x5 这样: 也可以把前后单词串联起来,让整个模型具有很强的关联性。比如: 你第一个 y1 就预测错了。那么之后的 y 很可能都预测错。(我的例子是:双色球概率)但是: 假如我们预测对了。那说明我们的模型的效果,已经特别好了(双色球每个球都预测对了~)
- 那我们就靠这 x 和 y 就能把前后语义都关联起来吗???
答:当然不仅于此。你别忘了我们还有贯穿横向的输入啊,如最开始 RNN 图的 a<0>. a<1> 这些 -
既然你说 y 是从词典选拔出来的词的概率属性。那么这个概率怎么算?
答:这问得太好了~~~前面说了: 一般都会预先给数据做 One-Hot 或 Embedding 编码。所以数据格式为: [0,0,....,1,...] # 只有一个为 1 基本上我们最后给输出都会套一层: softmax 激活函数,softmax 应该知道吧:e^x /(e^x1+..+e^x) 所以: softmax 结果就是一个 和 One-Hot 形状一样的概率列表集合: [....., 最高概率,...] softmax 的结果(概率列表):(代表着预测 在词典中每一个单词的可能性)
-
那么损失函数怎么算呢??
答:没错,损失函数也是我们最关注的。前面: 我们已经求出了 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||
或者你可以使用欧氏距离。
原始词嵌入并训练
- 假如我们通过一个句子的一部分来预测,这个句子的最后一个单词。
- 把词典的每个词做成 One-Hot 便是形式,记作矩阵 O
- 随机高维权重矩阵,记为 E
-
E @ O 矩阵乘积后记为词向量 W
可见如下案例:如果: 我们分词后词典总大小为 1000 那么: 他的 One-Hot 矩阵形状为 [6, 1000](假如我们这里通过句子 6 个词来预测最后一个词)并且: 随机高维权重矩阵 形状为 [1000, 300](注意,这个 300 是维度,可自行调整选择)注意: 上面权重矩阵是随机初始化的,后面训练调节的。最后: E @ O 后得到词向量 W 的形状为 [6, 300]
- 送进 NN(打成 1000 类) 作为输出
- 加一层 Softmax(算出 1000 个单词的概率)作为最终输出 y_predict
- y_predict 与 y 真正的单词标签(one-hot 后的)做交叉熵 loss
- 优化 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))