赞
踩
之前我们提到玻尔兹曼机(Boltzmann machine),波尔茨曼机作为一种基于能量函数的概率模型,因为能量函数比较复杂,所以存在较多的限制。虽然受限玻尔兹曼机(Restricted Boltzmann machine) 针对该问题,对能量函数进行进一步简化,即假设网络中仅有隐藏变量与观察变量的连接,而观察变量将没有连接,隐藏变量间也没有连接,且隐藏变量可用 n h n_h nh 个二进制随机变量表示,但是仍然存在限制问题。同时,该过程应用了马尔科夫链,导致计算成本较高。针对上述问题,Ian Goodfellow 于 2014 年提出了生成对抗网络(Generative Adversarial Network,GAN)模型,GAN 作为一类在无监督学习中使用的神经网络,有效地避免了马尔科夫链以及减低了波尔茨曼机所存在的限制问题,以至于在按文本生成图像、提高图片分辨率、药物匹配、检索特定模式的图片等任务中 GAN 的研究如火如荼。大牛Yann LeCun甚至评价GAN为 “adversarial training is the coolest thing since sliced bread”。
本文将通过一个简单的例子(发论文问题)向读者深入浅出的介绍 GAN 原理及其应用。
作为生成模型中的一种,生成对抗网络(Generative Adversarial Network,GAN)模型的训练过程可以被视为两个网络互相博弈的过程。下面我们将举一个简单的例子解释 GAN 的基本思想。
假设你是一门研究生,你想尽快地将实验结果写成一篇论文发表。
于是在每一次做完实验并写完初稿之后,都会跟你的导师进行沟通:你:boss,我实验结果出来,我想发论文
导师:(瞄了瞄你的实验结果之后) … 算了吧
(你通过跟其他论文的实验结果进行比较,发现自己的实验结果还偏低,于是,你又调整了实验参数,重新进行实验)
你:boss,我实验结果提高了,我想发论文
导师:… (瞄了瞄你的论文初稿之后)嗯 还有所欠缺
(你通过跟其他论文进行比较,发现自己写的论文初稿在表达方面还有所不足)
…
你:boss,我想发论文
导师:… (仔细看了看你的论文之后)嗯 可以试一试
(通过这样不断的修改和被拒绝,你的论文最终获得了导师的赞赏与肯定)
通过上面的例子,大家应该对 GAN 的思想有一个比较感性的认识了吧,下面我们可以进一步对 GAN 的基本结构和思想进行介绍。
GAN 的主要结构包括一个生成器 G(Generator)和一个判别器 D(Discriminator)。
在上面的例子中,研究生相对于生成器。在一开始的时候,他只是一个什么都不懂的初学者,为了能让该研究生发出好的 paper,需要给他裴蓓一个导师来指导他做实验写论文,并告诉他 paper 面前的质量,通过反复的修改和被拒绝,paper 最终达到了可以投稿的标准,而这个导师就相当于生成对抗网络 GAN 中的判别器。
生成对抗网络 GAN 主要包含两个模块:生成器 G(Generator)和一个判别器 D(Discriminator)。生成对抗网络 GAN 中所描述的对抗,其实就是指生成网络与判别网络之间的相互对抗。以下图为例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4YmQ6UU-1677247913507)(img/微信截图_20200805232522.png)]
生成器模型(上图中蓝色部分 Generator)的主要工作就是学习真实图片集数据,从而使自己生成的图片更加接近与真实图片,以到达“以假乱真”,也就是“欺骗”判别器。
判别器模型(上图中红色部分 Discriminator)的主要工作就是从图片集中找出生成器所生成的图片,并区分该图片与真实图片的差异,以进行真假判别。
在整个迭代过程中,生成器不断的生成越来越逼真的图片,而判别器不断额努力鉴别出图片的真假。该过程可以视为两个网络互相博弈的过程,随着迭代次数的增加,最终两者将会趋于平衡,也就是说生成器能够生成出和真实图片一模一样的的图片,而判别器已经很难从图片集中辨别出生成器所生成的假图片了。也就是说,对于图片集中的每一张图片,判别器都给出接近 0.5 的概率认为该图片是真实的。
生成器模型的任务:首先需要将一个 n n n 维向量输入生成器模型,然后输出一个图片像素大小的图片(这里,生成器模型可以是任意可以输出图片的模型,如全连接神经网络,反卷积神经网络等)。
注:输入向量:携带输出的某些信息,这些信息可以是手写数字为数字几,手写的潦草程度等。由于这里我们对于输出数字的具体信息不做要求,只要求其能够最大程度与真实手写数字相似(能骗过判别器)即可。所以我们使用随机生成的向量来作为输入即可,这里面的随机输入最好是满足常见分布比如均值分布,高斯分布等。
判别器模型的任务:主要能够辨别输入的图片的真假都可以作为判别器。
前面分别介绍了生成器和判别器的任务,在这一节,我们将主要介绍生成对抗网络的训练过程,其基本流程如下:
step 1: 初始化:对判别器 D 的参数 θ d \theta_d θd 和生成器 G 的参数 θ g \theta_g θg;
step 2: 生成器“伪造”生成样本:首先,从真实样本中采样 m m m 个样本 { x 1 , x 2 , … x m } \left\{x^1, x^2, \ldots x^m\right\} {x1,x2,…xm};然后,从先验分布噪声中采样 m m m 个噪声样本 z 1 , z 2 , … , z m {z^1, z^2, \ldots, z^m} z1,z2,…,zm;接下去,利用生成器“伪造” m m m 个新样本 { x ~ 1 , x ~ 2 , … , x ~ m } \left\{\tilde{\boldsymbol{x}}^1, \tilde{\boldsymbol{x}}^2, \ldots, \tilde{\boldsymbol{x}}^m\right\} {x~1,x~2,…,x~m};最后,固定生成器 G。
step 3:判别器“鉴别”生成样本:通过对判别器 D 进行训练,以让它尽可能准确的“鉴别”出生成样本。
step 4: “欺骗”判别器:循环更新判别器 k 次之后,再利用较小的学习率来更新一次生成器的参数。使得判别器已经很难从样本集中辨别出生成器所生成的生成样本了。也就是说,对于样本集中的每一个样本,判别器都给出接近 0.5 的概率认为该样本是真实的。
注:为什么是先训练判别器再训练生成器呢?
以上面的导师和学生的例子吧,学生(生成器)要写出一篇好的 paper (生成样本),那么就需要有一个能够较好的区分好 paper (真实样本)和坏 paper (生成样本)的好导师(判别器)之后,才能指导学生(生成器)如何对 paper (生成样本)进行优化。
前面已经对生成对抗网络进行介绍,接下去,我们将从理论基础方面介绍生成对抗网络的训练过程。
首先,需要从优化目标函数开始介绍,其表达式如下所示:
min G max D V ( G , D ) = min G max D E x ∼ p data [ log D ( x ) ] + E z ∼ p z [ log ( 1 − D ( G ( z ) ) ] \min _{G} \max _D V(G, D)=\min _G \max _D \mathbb{E}_{x \sim p_{\text { data }}}[\log D(x)]+\mathbb{E}_{z \sim p_z}[\log (1-D(G(z))] GminDmaxV(G,D)=GminDmaxEx∼p data [logD(x)]+Ez∼pz[log(1−D(G(z))]
对于判别式而言,其主要用于区别样本的真伪,所以可以视为是一个二分类问题,上式中所使用的 V ( G , D ) V(G, D) V(G,D)为二分类问题中常见的交叉熵损失。公式如下所示:
H ( p , q ) : = − ∑ i p i log q i H(p, q) :=-\sum_i p_i \log q_i H(p,q):=−i∑pilogqi
p i p_i pi 和 q i q_i qi 为真实的样本分布和生成器的生成分布。
对于生成器 G 而言,为了尽可能欺骗 D,所以需要最大化生成样本的判别概率 D ( G ( z ) ) D(G(z)) D(G(z)),即最小化 l o g ( 1 − D ( G ( z ) ) ) log(1-D(G(z))) log(1−D(G(z)))。
注意: l o g ( D ( x ) ) log(D(x)) log(D(x)) 一项与生成器 G 无关,所以可以忽略。
实际训练过程中,生成器和判别器采用交替训练的方式进行。因为对于生成器,其最小化为 max D V ( D , G ) \max _{D} V(D, G) maxDV(D,G),即最小化 $ V(D, G) $的最大值。所以为了保证 $ V(D, G) $ 取得最大值,需要对判别器迭代训练 k k k 次,然后再训练一次生成器。
当生成器 G 固定时,我们可以对 V ( D , G ) V(D,G) V(D,G) 求导,求出最优判别器 D ∗ ( x ) D*(x) D∗(x):
D ∗ ( x ) = p g ( x ) p g ( x ) + p d a t a ( x ) D^{*}(x)=\frac{p_{g}(x)}{p_{g}(x)+p_{d a t a}(x)} D∗(x)=pg(x)+pdata(x)pg(x)
把最优判别器代入上述目标函数,可以进一步求出在最优判别器下,生成器的目标函数等价于优化 p d a t a ( x ) p_{d a t a}(x) pdata(x) , p g ( x ) p_{g}(x) pg(x) 的 JS 散度(JSD, Jenson Shannon Divergence)。
可以证明,当 G,D 二者的 capacity 足够时,模型会收敛,二者将达到纳什均衡。此时, p d a t a ( x ) p_{d a t a}(x) pdata(x)= p g ( x ) p_{g}(x) pg(x),判别器不论是对于 p d a t a ( x ) p_{d a t a}(x) pdata(x) 还是 p g ( x ) p_{g}(x) pg(x) 中采样的样本,其预测概率均为 1/2,即生成样本与真实样本达到了难以区分的地步。
通过上述min max的博弈过程,理想情况下会收敛于生成分布拟合于真实分布。
本文首先,通过以一个学生发 paper 的 example 的方式引入了生成对抗网络;然后,并进一步介绍了生成对抗网络的框架和思想,中生成器和判别器;最后,通过介绍生成对抗网络的训练过程,以引入生成对抗网络的训练公式。
根据通用近似定理,前馈网络和循环网络都有很强的能力。但为什么还要引入注意力机制呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6fq7qAC-1677247913525)(img/20200916172920.png)]
随着 Attention 提出 开始,就被 广泛 应用于 各个领域。比如:自然语言处理,图片识别,语音识别等不同方向深度学习任务中。随着 【Transformer 】的提出,Attention被 推向了圣坛。
Soft Attention:传统的 Attention 方法,是参数化的(Parameterization),因此可导,可以被嵌入到模型中去,直接训练。梯度可以经过Attention Mechanism模块,反向传播到模型其他部分。
Hard Attention:一个随机的过程。Hard Attention不会选择整个encoder的输出做为其输入,Hard Attention会依概率Si来采样输入端的隐状态一部分来进行计算,而不是整个encoder的隐状态。为了实现梯度的反向传播,需要采用蒙特卡洛采样的方法来估计模块的梯度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kdSxU2w0-1677247913526)(img/微信截图_20210109115925.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5luXLMS0-1677247913527)(img/微信截图_20210109120012.png)]
注:具体内容可以参考 self-attention 长怎么样?
贡献者:天骄,小猪呼噜,沐风,杨夕,芙蕖,李玲
在卷积神经网络中,channel的含义是每个卷积层中卷积核的数量。卷积层的卷积个数就等于卷积层输出的out_channels。这个值也与下一层卷积的in_channels相同。下面举例说明。
如下图,假设现有一个为 6×6×3 的图片样本,使用 3×3×3 的卷积核(filter)进行卷积操作。此时输入图片的 channels
为 3 ,而卷积核中的 in_channels
与 需要进行卷积操作的数据的 channels
一致(这里就是图片样本,为3)。
接下来,进行卷积操作,卷积核中的27个数字与分别与样本对应相乘后,再进行求和,得到第一个结果。依次进行,最终得到 4×4的结果。
上面步骤完成后,由于只有一个卷积核,所以最终得到的结果为 4×4×1, out_channels
为 1 。
在实际应用中,都会使用多个卷积核。这里如果再加一个卷积核,就会得到 4×4×2 的结果。
总结一下, channels
分为三种:
channels
,取决于图片类型,比如RGB;out_channels
,取决于卷积核的数量。此时的 out_channels
也会作为下一次卷积时的卷积核的 in_channels
;in_channels
,就是上一次卷积的 out_channels
,如果是第一次做卷积,就是样本图片的 channels
。池化层针对区域是非重叠区域。
除降低参数量外,能够保持对平移、伸缩、旋转操作的不变性
CNN 能够捕获文本中的局部特征
平移不变性:即目标在空间内发生平移,但是结果(标签)不变
平移不变性=卷积+最大池化。卷积层具有平移等变性,池化层具有平移不变性。卷积神经网络的平移不变性,是池化层赋予的。
如果一个函数满足“输入改变,输出也以同样的方式进行改变”这一性质,我们就说该函数是等变的(equivariant)。形式化地,如果函数 f ( x ) f(x) f(x)与 g ( x ) g(x) g(x)满足 f ( g ( x ) ) = g ( f ( x ) ) f(g(x))=g(f(x)) f(g(x))=g(f(x)),我们就说 f ( x ) f(x) f(x)对于变换 g g g具有等变性。
卷积层具有平移等变性,也就是说卷积层对平移是敏感的,输入的平移能等价地影响输出。直观地,如果把一张输入图像先平移后卷积,其结果与先卷积后平移效果是一样的。如果我们移动输入图像中的物体,它的表示也会在输出中移动同样的量。
卷积层的参数共享(Parameter Sharing)特性使得卷积层具有平移等变性。参数共享是指在一个模型的多个函数中使用相同的参数。在传统的神经网络中,当计算一层的输出时,权重矩阵的每个元素只使用一次。当它乘以输入的一个元素后,就再也不会用到了。但是在卷积神经网络中,卷积核的每一个元素都作用在输入的每一个位置上。卷积运算中的参数共享保证了我们只需要学习一个参数集合,而不是对于每个位置都需要学习一个单独的参数集合。
如果一个函数满足“输入改变,输出不会受影响”这一性质,我们就说该函数是不变的(invariant)。形式化地,如果函数 f ( x ) f(x) f(x)与 g ( x ) g(x) g(x)满足 g ( x ) = x ′ g(x)=x' g(x)=x′且 f ( x ) = f ( x ′ ) = f ( g ( x ) ) f(x)=f(x')=f(g(x)) f(x)=f(x′)=f(g(x)),我们就说 f ( x ) f(x) f(x)对于变换 g g g具有不变性。
池化层具有(近似)平移不变性,也就是说池化层对平移不敏感。不管采用什么样的池化函数,当输入做出少量平移时,池化能够帮助输入的表示近似不变。例如我们使用最大池化,只要变换不影响到最大值,我们的池化结果不会收到影响。对于一个卷积核来说,只有一个值的变动会影响到输出, 其他的变换都不会造成扰动。平均池化的近似不变性就稍弱些。
局部平移不变性是一个很有用的性质,尤其是当我们关心某个特征是否出现而不关心它出现的具体位置时。
卷积的运算量十分巨大,如果将卷积运算转化为矩阵运算,便能利用GPU来提升性能。
im2col全称image to column(从图像到矩阵),作用为加速卷积运算。即把包含批数量的4维数据转换成2维数据。也就是将输入数据降维,然后通过numpy的矩阵运算后得到结果,再将结果的形状还原,从而通过用矩阵运算来代替for循环语句。
im2col
将卷积核和卷积核扫过的区域都转化为列(行)向量。举个例子方便理解:
假设一个3*3卷积核为:
输入图像是5*5像素的单通道图像:
卷积核会锁定3*3的滑动窗口:
im2col
会将每个滑动窗口内的像素转为列向量:
就这样转化所有滑动窗口的列向量,将其拼接成9行的矩阵(行数与滑动窗口数目相关,列数则与卷积核大小相关):
im2col
还会将卷积核转化为行向量:
最后使用卷积核行向量乘以滑动窗口元素组成的矩阵:
得到1*9的列向量,将其拼接成特征图:
具体地,空洞卷积通过给卷积核插入“空洞”变相增加其大小。如果在卷积核的每两个元素之间插入 D − 1 D-1 D−1个空洞,该卷积核的有效大小为 K ′ = K + ( K − 1 ) × ( D − 1 ) K'=K+(K-1)\times (D-1) K′=K+(K−1)×(D−1)。其中 K K K为原始卷积核大小, D D D称为膨胀率(扩张率,dilation rate)。当 D = 1 D=1 D=1时卷积核为普通的卷积核。
例如下图中
例如卷积核大小为3,步长为1,膨胀率为2的空洞卷积动态示意图:
1.反卷积原理
反卷积=上采样=(转置卷积+微步卷积)⊆ 空洞卷积=一般意义上的广义卷积(包含上采样和下采样)。
上采样:在应用在计算机视觉的深度学习领域,由于输入图像通过卷积神经网络(CNN)提取特征后,输出的尺寸往往会变小,而有时我们需要将图像恢复到原来的尺寸以便进行进一步的计算(e.g.:图像的语义分割),这个采用扩大图像尺寸,实现图像由小分辨率到大分辨率的映射的操作,叫做上采样(Upsample)。
反卷积是一种特殊的正向卷积,先按照一定的比例通过补0来扩大输入图像的尺寸,接着旋转卷积核,再进行正向卷积。
反卷积又被称为Transposed(转置) Convolution,其实卷积层的前向传播过程就是反卷积层的反向传播过程,卷积层的反向传播过程就是反卷积层的前向传播过程。因为卷积层的前向反向计算分别为乘 C C C 和 C T C^T CT,而反卷积层的前向反向计算分别为乘 C T C^T CT 和 ( C T ) T (C^T)^T (CT)T,所以它们的前向传播和反向传播刚好交换过来。
上图展示一个反卷积的工作过程,乍看一下好像反卷积和卷积的工作过程差不多,主要的区别在于反卷积输出图片的尺寸会大于输入图片的尺寸,通过增加padding来实现这一操作,上图展示的是一个strides(步长)为1的反卷积。下面看一个strides不为1的反卷积
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rVOeXpGK-1677247913540)(https://i2.kknews.cc/SIG=2stfug5/ctp-vzntr/q0r0n688rps74822ornn58958rqrr4r1.jpg)]
上图中的反卷积的stride为2,通过间隔插入padding来实现的。同样,可以根据反卷积的o、s、k、p参数来计算反卷积的输出i,也就是卷积的输入。公式如下:i=(o−1)∗s+k−2∗p。
2.作用
通过反卷积可以用来可视化卷积的过程
反卷积在GAN等领域中有着大量的应用。 比如用GANs生成图片,其中的generator和discriminator均采用深度学习,generator生成图片过程中采用的就是反卷积操作(当然discriminator采用卷积对generator生成的图片判别真伪)。
将一些低分辨率的图片转换为高分辨率的图片。
参考:
https://kknews.cc/code/zb2jr43.html
https://zhuanlan.zhihu.com/p/48501100
无论是 全连接网络 还是 卷积神经网络 他们的前提假设都是 元素间相互独立,也就是输入和输出的一一对应,也就是一个输入得到一个输出。不同的输入之间是没有联系的。
然而,在 序列数据(自然语言处理任务、时间序列任务)中,对于每一个输出,他不仅和他所对应的输入相关,还与前面其他词 和 词间的顺序相关。
这个时候,全连接网络 还是 卷积神经网络 将不能很好解决该问题。
RNN 之所以被称为"循环",是因为它对序列中的每个元素执行相同的任务,输出取决于先前的计算。考虑RNN的另一种方式是它们有一个“记忆”,它可以捕获到目前为止计算的信息。
从 上面图片中,可以看出 RNN网络 的 核心在于 s t s_t st 的值 不仅取决 xt,而且还与 s t − 1 s_{t-1} st−1 相关。
!
注:f 为隐藏层的激活函数,一般用 softmax 函数
注:g 为输出层的激活函数,一般用 tanh 函数 或者 ReLU 函数
误差沿时间反向传播:
矩阵的模的值取决 于 上图 红框部分:
当其大于1时,随着RNN深度的增加,矩阵的模的值呈指数函数增加,此时将出现 梯度爆炸;
当其小于1时,随着RNN深度的增加,矩阵的模的值呈指数函数减少,此时将出现 梯度消失;
导致训练时梯度不能在较长序列中一直传递下去,从而使RNN无法捕捉到长距离的影响
RNN 梯度消失和梯度爆炸问题
引用门控机制
原理:门实际上就是一层全连接层,它的输入是一个向量,输出是一个0到1之间的实数向量。
δ 是 sigmoid 函数
序列长度为T,隐藏层维度为H
O(T * H^2)
计算量大
LSTM 计算量大
候选激活值(candidate activation)的计算
更新门(update gate)的计算
GRU输入输出的结构与普通的RNN相似,其中的内部思想与LSTM相似。
与LSTM相比,GRU内部少了一个”门控“,参数比LSTM少,但是却也能够达到与LSTM相当的功能。考虑到硬件的计算能力和时间成本,因而很多时候我们也就会选择更加”实用“的GRU啦。
为什么要有 Transformer? 首先需要知道在 Transformer 之前都有哪些技术,这些技术所存在的问题:
基于Transformer的架构主要用于建模语言理解任务,它避免了在神经网络中使用递归,而是完全依赖于self-attention机制来绘制输入和输出之间的全局依赖关系。
Transformer 整体结构
从上一张 Transformer 结构图,可以知道 Transformer 是一个 encoder-decoder 结构,但是 encoder 和 decoder 又包含什么内容呢?
Transformer encoder-decoder 结构
其中上图中每一层的内部结构如下图所求。
具体内容,后面会逐一介绍。
Transformer encoder-decoder 内部结构
class Transformer(tf.keras.Model):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size, target_vocab_size, pe_input, pe_target, rate=0.1):
super(Transformer, self).__init__()
# Encoder 模块
self.encoder = Encoder(num_layers, d_model, num_heads, dff, input_vocab_size, pe_input, rate)
# Decoder 模块
self.decoder = Decoder(num_layers, d_model, num_heads, dff, target_vocab_size, pe_target, rate)
# 全连接层
self.final_layer = tf.keras.layers.Dense(target_vocab_size)
def call(self, inp, tar, training, enc_padding_mask, look_ahead_mask, dec_padding_mask):
# step 1: encoder
enc_output = self.encoder(inp, training, enc_padding_mask)
# step 2:decoder
dec_output, attention_weights = self.decoder(tar, enc_output, training, look_ahead_mask, dec_padding_mask)
# step 3:全连接层
final_output = self.final_layer(dec_output)
return final_output, attention_weights
Transformer encoder 结构
class EncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, dff)
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
def call(self, x, training, mask):
# step 1:多头自注意力
attn_output, _ = self.mha(x, x, x, mask)
# step 2:前馈网络
attn_output = self.dropout1(attn_output, training=training)
# step 3:Layer Norml
out1 = self.layernorm1(x + attn_output)
# step 4:前馈网络
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
# step 5:Layer Norml
out2 = self.layernorm2(out1 + ffn_output)
return out2
class Encoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size, maximum_position_encoding, rate=0.1):
super(Encoder, self).__init__()
self.d_model = d_model
self.num_layers = num_layers # encoder 层数
# 词嵌入
self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
# 位置编码
self.positional_encoding_obj = Positional_Encoding()
self.pos_encoding = self.positional_encoding_obj.positional_encoding(maximum_position_encoding, self.d_model)
# Encoder 模块构建
self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate) for _ in range(num_layers)]
self.dropout = tf.keras.layers.Dropout(rate)
def call(self, x, training, mask):
seq_len = tf.shape(x)[1]
# step 1:词嵌入
x = self.embedding(x)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
# step 2:位置编码
x += self.pos_encoding[:, :seq_len, :]
x = self.dropout(x, training=training)
# step 3:Encoder 模块构建
for i in range(self.num_layers):
x = self.enc_layers[i](x, training, mask)
return x
Transformer decoder 结构
class DecoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(DecoderLayer, self).__init__()
self.mha1 = MultiHeadAttention(d_model, num_heads)
self.mha2 = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, dff)
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
self.dropout3 = tf.keras.layers.Dropout(rate)
def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
# step 1:带 sequence mask 的 多头自注意力
attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask)
attn1 = self.dropout1(attn1, training=training)
# step 2:Layer Norm
out1 = self.layernorm1(attn1 + x)
# step 3:带 padding mask 的 多头自注意力
attn2, attn_weights_block2 = self.mha2(enc_output, enc_output, out1, padding_mask)
attn2 = self.dropout2(attn2, training=training)
# step 4:Layer Norm
out2 = self.layernorm2(attn2 + out1)
# step 5:前馈网络
ffn_output = self.ffn(out2)
ffn_output = self.dropout3(ffn_output, training=training)
# step 6:Layer Norm
out3 = self.layernorm3(ffn_output + out2)
return out3, attn_weights_block1, attn_weights_block2
class Decoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, target_vocab_size, maximum_position_encoding, rate=0.1):
super(Decoder, self).__init__()
self.d_model = d_model
self.num_layers = num_layers # encoder 层数
# 词嵌入
self.embedding = tf.keras.layers.Embedding(target_vocab_size, d_model)
# 位置编码
self.positional_encoding_obj = Positional_Encoding()
self.pos_encoding = self.positional_encoding_obj.positional_encoding(maximum_position_encoding, d_model)
# Dncoder 模块构建
self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate) for _ in range(num_layers)]
self.dropout = tf.keras.layers.Dropout(rate)
def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
seq_len = tf.shape(x)[1]
attention_weights = {}
# step 1:词嵌入
x = self.embedding(x)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
# step 2:位置编码
x += self.pos_encoding[:, :seq_len, :]
x = self.dropout(x, training=training)
# step 3:Dncoder 模块构建
for i in range(self.num_layers):
x, block1, block2 = self.dec_layers[i](x, enc_output, training, look_ahead_mask, padding_mask)
attention_weights['decoder_layer{}_block1'.format(i+1)] = block1
attention_weights['decoder_layer{}_block2'.format(i+1)] = block2
return x, attention_weights
在介绍 self-attention 之前,首先需要介绍一下 传统的 attention,其结构如下图所示:
传统的 attention 结构
L x = ∣ ∣ S o u r c e ∣ ∣ L_x=||Source|| Lx=∣∣Source∣∣代表Source的长度
传统的 attention 流程
step 1:计算权值系数
采用 不同的函数或计算方式,对 query 和 key 进行计算,求出相似度或相关性
采用的计算方法:
step 2: softmax 归一化
step 3: 加权求和
self-attention 计算公式
self-attention 结构图
一句话概述:每个位置的embedding对应 Q,K,V 三个向量,这三个向量分别是embedding点乘 WQ,WK,WV 矩阵得来的。每个位置的Q向量去乘上所有位置的K向量,其结果经过softmax变成attention score,以此作为权重对所有V向量做加权求和即可。
具体步骤
举例
优点
代码讲解【注:代码采用 tensorflow 框架编写】
def scaled_dot_product_attention(q, k, v, mask):
# s1:权重 score 计算:查询向量 query 点乘 key
matmul_qk = tf.matmul(q, k, transpose_b=True)
# s2:scale 操作:除以 sqrt(dk),将 Softmax 函数推入梯度极小的区域
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
# s3:
if mask is not None:
scaled_attention_logits += (mask * -1e9)
# s4:Softmax 归一化
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)
# s5:加权求和
output = tf.matmul(attention_weights, v)
return output, attention_weights
因为Q、K、V的不同,可以保证在不同空间进行投影,增强了表达能力,提高了泛化能力。
主要原因:在理论上,加性模型和点积模型的复杂度差不多,但是点积模型在实现上可以更好地利用矩阵乘法,而矩阵乘法有很多加速策略,因此能加速训练。但是论文中实验表明,当维度 d d d越来越大时,加性模型的效果会略优于点积模型,原因应该是加性模型整体上还是比点积模型更复杂(有非线性因素)。
一句话回答:当输入信息的维度 d 比较高,会导致 softmax 函数接近饱和区,梯度会比较小。因此,缩放点积模型可以较好地解决这一问题。
具体分析:
如果某个
相对于其他元素很大的话,那么对此向量softmax后就容易得到一个onehot向量,不够“soft”了,而且反向传播时梯度为0会导致梯度消失;
下面,我们将会围绕着几个问题,进行一一解答。
基于卷积网络和循环网络的变长序列编码
全连接模型和自注意力模型
双线性点积模型使用Q,K两个向量,而不是只用一个Q向量,这样引入非对称性,更具健壮性(Attention对角元素值不一定是最大的,也就是说当前位置对自身的注意力得分不一定最高)。
初始化 N 组 Q , K , V Q,K,V Q,K,V矩阵
合并 8 个矩阵
最后,让我们来看一下完整的流程:
完整的流程
换一种表现方式:
multi-head attention 表示方式
multi-head attention 动图展示
为了让 Transformer 能够注意到不同子空间的信息,从而捕获到跟多的特征信息。【本质:实验定律】
类似于CNN中通过多通道机制进行特征选择。Transformer中使用切头(split)的方法,是为了在不增加复杂度( O ( n 2 d ) O(n^2 d) O(n2d))的前提下享受类似CNN中“不同卷积核”的优势。
Transformer的多头注意力看上去是借鉴了CNN中同一卷积层内使用多个卷积核的思想,原文中使用了 8 个“scaled dot-product attention”,在同一“multi-head attention”层中,输入均为“KQV”,同时进行注意力的计算,彼此之前参数不共享,最终将结果拼接起来,这样可以允许模型在不同的表示子空间里学习到相关的信息,在此之前的 A Structured Self-attentive Sentence Embedding 也有着类似的思想。简而言之,就是希望每个注意力头,只关注最终输出序列中一个子空间,互相独立。其核心思想在于,抽取到更加丰富的特征信息。
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
assert d_model % self.num_heads == 0
self.depth = d_model // self.num_heads
# 初始化 Q,K,V 矩阵
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, v, k, q, mask):
batch_size = tf.shape(q)[0]
# step 1:利用矩阵计算 q,k,v
q = self.wq(q)
k = self.wk(k)
v = self.wv(v)
# step 2:
q = self.split_heads(q, batch_size)
k = self.split_heads(k, batch_size)
v = self.split_heads(v, batch_size)
# step 3:每组 分别 进行 self-attention
scaled_attention, attention_weights = scaled_dot_product_attention(
q, k, v, mask)
# step 4:矩阵拼接
scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])
concat_attention = tf.reshape(scaled_attention, (batch_size, -1, self.d_model))
# step 5:全连接层
output = self.dense(concat_attention)
return output, attention_weights
位置编码(Position encoding) 结构图
注:
pos 表示当前词在句子中的位置
i 表示向量中每个值 的 index
在偶数位置:使用 正弦编码 sin();
在奇数位置:使用 余弦编码 cos();
因为 [ W 1 W 2 ] [ e ; p ] = W 1 e + W 2 p , W ( e + p ) = W e + W p [W1 W2][e; p] = W1e + W2p,W(e+p)=We+Wp [W1W2][e;p]=W1e+W2p,W(e+p)=We+Wp,就是说求和相当于拼接的两个权重矩阵共享(W1=W2=W),但是这样权重共享是明显限制了表达能力的。
# 位置编码 类
class Positional_Encoding():
def __init__(self):
pass
# 功能:计算角度 函数
def get_angles(self, position, i, d_model):
'''
功能:计算角度 函数
input:
position 单词在句子中的位置
i 维度
d_model 向量维度
'''
angle_rates = 1 / np.power(10000, (2 * (i // 2)) / np.float32(d_model))
return position * angle_rates
# 功能:位置编码 函数
def positional_encoding(self, position, d_model):
'''
功能:位置编码 函数
input:
position 单词在句子中的位置
d_model 向量维度
'''
angle_rads = self.get_angles(
np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model
)
# apply sin to even indices in the array; 2i
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
# apply cos to odd indices in the array; 2i+1
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
pos_encoding = angle_rads[np.newaxis, ...]
return tf.cast(pos_encoding, dtype=tf.float32)
BN 计算公式
LN 计算公式
原始BN是为CNN而设计的,对整个batchsize范围内的数据进行考虑;
对于RNN来说,sequence的长度是不一致的,所以用很多padding来表示无意义的信息。如果用 BN 会导致有意义的embedding 损失信息。
所以,BN一般用于CNN,而LN用于RNN。
layernorm是在hidden size的维度进行的,跟batch和seq_len无关。每个hidden state都计算自己的均值和方差,这是因为不同hidden state的量纲不一样。beta和gamma的维度都是(hidden_size,),经过白化的hidden state * beta + gamma得到最后的结果。
LN在BERT中主要起到白化的作用,增强模型稳定性(如果删除则无法收敛)
class EncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
...
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
...
def call(self, x, training, mask):
...
# step 3:Layer Norml
out1 = self.layernorm1(x + attn_output)
# step 4:前馈网络
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
# step 5:Layer Norml
out2 = self.layernorm2(out1 + ffn_output)
return out2
# 功能: padding mask
def create_padding_mask(seq):
'''
功能: padding mask
input:
seq 序列
'''
seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
return seq[:, tf.newaxis, tf.newaxis, :]
sequence mask 公式
注意力矩阵, 每个元素 a i j a_{ij} aij 代表 第 i 个词和第 j 个词的内积相似度
下三角矩阵,上三角的值全为0,下三角全是 1
注:
在 decoder 的 scaled dot-product attention 中,里面的 attn_mask = padding mask + sequence mask
在 encoder 的 scaled dot-product attention 中,里面的 attn_mask = padding mask
# 功能:sequence mask
def create_look_ahead_mask(size):
'''
功能: sequence mask
input:
seq 序列
'''
mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
return mask
答:Transformer在抛弃了 LSTM 结构后,FFN 中的激活函数成为了一个主要的提供非线性变换的单元。
答:
具体说明:
我们将神经元的输入 x 乘上一个服从伯努利分布的 m 。而该伯努利分布又是依赖于 x 的:
其中, X~N(0,1),那么 φ(x) 就是标准正态分布的累积分布函数。这么做的原因是因为神经元的输入 x 往往遵循正态分布,尤其是深度网络中普遍存在Batch Normalization的情况下。当x减小时, φ(x) 的值也会减小,此时x被“丢弃”的可能性更高。所以说这是随机依赖于输入的方式。
现在,给出GELU函数的形式:
其中 φ(x) 是上文提到的标准正态分布的累积分布函数。因为这个函数没有解析解,所以要用近似函数来表示。
图像
导数
所以,GELU的优点就是在ReLU上增加随机因素,x越小越容易被mask掉。
FFN的gelu激活函数和self-attention,注意self-attention是非线性的(因为有相乘和softmax)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。