深度学习那些事

现今使用深度学习的方法解决 NLP 领域的问题变得越来越流行,近年来,学界上出现了越来越多有关深度学习的论文; 业界上各种深度学习框架、深度学习应用也被推出。

本文主要分享本人对于深度学习以及自然语言的理解及一些经验。

大纲

  • 什么是深度学习?
  • 热门的深度学习框架及选择建议
  • 常用深度学习模块原理及应用
    • CNN
    • RNN及其变体
    • Attention机制的原理以及实现
    • Transformer
  • 常用的网络训练技巧
    • 正则化的意义及使用方式
    • 规范化的作用及常用的规范化方式
    • 常见的激活函数及选择
    • 常见的优化器及选择
    • mask 的作用
    • 模型效果提升偏方
      • 迁移学习
      • 鉴别学习
      • 模型融合
      • 模型蒸馏
  • 深度学习与NLP
    • word2vec vs fasttext
    • 如何给网络添加特征
    • ELMo、GPT、BERT
  • TensorFlow 实战:以DPCNN为例讲述如何实现一篇论文
  • 写在最后
  • Refrence

什么是深度学习?

深度学习 \(\neq\) 深度学习框架,掌握深度学习首先要掌握相关的理论知识,掌握常用的网络模型,锻炼自己的工程能力。做到看得懂论文,还能自己实现论文。

现在流行的深度学习方法主要是基于反向传播(Back-Propagation, BP)思想的,主要是深度神经网络 (DNN,Deep Neural Network),深度学习一般在大数据量下才能体现出其优点,而且深度学习对计算资源要求也比较高,一般需要用到 GPU (Graphics processing unit) 加速运算。

神经网络

神经网络表示

  • 输入层:输入特征被称作神经网络的输入层 (Input Layer)
  • 隐藏层:一般在输入层和输出层之间的网络层都可称为隐藏层,“隐藏”的含义是中间节点的真正数值是无法看到的。
  • 输出层:将网络空间最终映射到任务空间

如图是一个双层神经网络(一般计算网络的层数时,通常不考虑输入层,因此图中隐藏层是第一层,输出层是第二层),也称作单隐层神经网络,隐藏层中每个结点称为神经元,神经元是含有非线性激活函数的感知单元。

其中 \(a^{[0]}, a^{[1]}, .., a^{[layer\_size]}\) 等为每一层的输出值,如 \(a^{[1]} = [a_{0}^{[1]}, a_{1}^{[1]}, ..., a_{hidden\_size}^{[1]}]\) 为第一层的输出值。

神经网络的计算

神经网络中的计算由前向传播与反向传播构成,一般由计算图表示。

前向传播

前向传播是计算神经网络输出的过程,对于单个神经元有:

其中 \(W\) 是一个向量,\(W^{T}\) 是向量的转置, \(b\) 是一个标量

对于隐藏层的第一个结点有:

\[z_{1}^{[1]} = (W_{1}^{[1]})^{T}X + b_{1}^{[1]} \\\\ a_{1}^{[1]} = \sigma(z_{1}^{[1]}) \\\\ where\ X = \begin{bmatrix} x_{1} \\\\ x_{2} \\\\ x_{3} \end{bmatrix}, W_{1}^{[1]} \in \mathbb{R}^{3\times 1}, b_{1}^{[1]}\ is\ a\ scalar\]

依次类推对于第一个隐藏层整体有:

\[z^{[1]} = (W^{[1]})^{T} a^{[0]} + b^{[1]} \\\\ a^{[1]} = \sigma(z^{[1]}) \\\\ where\ (W^{[1]})^{T} = \begin{bmatrix} (W_{1}^{[1]})^{T} \\\\ (W_{2}^{[1]})^{T} \\\\ (W_{3}^{[1]})^{T} \\\\ (W_{4}^{[1]})^{T} \end{bmatrix} \in \mathbb{R}^{4\times 3}, b^{[1]} = \begin{bmatrix} b_{1}^{[1]} \\\\ b_{2}^{[1]} \\\\ b_{3}^{[1]} \\\\ b_{4}^{[1]} \end{bmatrix} \in \mathbb{R}^{4\times 1}\]

同理对于输出层有

\[z^{[2]} = (W^{[2]})^{T} a^{[1]} + b^{[2]} \\\\ \hat{y} = a^{[2]} = \sigma(z^{[2]}) \\\\ where\ (W^{[2]})^{T} \in \mathbb{R}^{1\times 4}, b^{[2]}\in \mathbb{R}^{1\times 1}\]

反向传播

所谓的反向传播(Back Propagation)即是当我们需要计算最终值相对于某个特征变量的导数时,我们需要利用计算图中上一步的结点定义。

如图

其中 \(L(a, y)\) 为损失函数,损失函数计算了预测值 \(a\) 与真实值 \(y\) 的差距。这里假设 \(L(a, y)\) 损失函数为

\[L(a, y) = -(y log a) - (1-y)log(1-a)\]

反向传播过程如下:

首先反向求出 \(L\) 对于 \(a\) 的导数:

\[d(a) = \frac{d L(a, y)}{da} = - \frac{y}{a} + \frac{1-y}{1-a}\]

然后继续反向求出 \(L\)\(z\) 的导数,根据链式求导法则有

\[dz = \frac{dL}{dz} = \frac{dL}{da}\frac{da}{dz} = a-y\]

依次类推求出最终损失函数相对于原始参数的导数之后,使用梯度下降方式优化更新参数

\[w_{1} := w_{1} - \alpha dw_{1} \\\\ w_{2} := w_{2} - \alpha dw_{2} \\\\ b := b - \alpha db\]

其中 \(\alpha\) 为优化器的学习速率

神经网络学习建议

建议自己动手实现一个简易的神经网络框架 (可以基于 python 的 numpy 库) 实现前向传播和反向传播过程,这样有助于对神经网络的理解。

热门的深度学习框架及选择建议

深度学习框架主要有两种类型:静态图和动态图,静态图的代表框架是 Google 开源的 TensorFlow (简称 TF) , 动态图的代表框架是 Facebook 开源的 PyTorch (简称 PT),上述两种框架也是如今最热门的两种框架生态。

以 TensorFlow 为代表的框架生态主要有:

  • TensorFlow
  • TensorFlow Eager: TF 的动态图版本
  • Keras: 基于 TF 的高级模块框架,已被高度继承在新版的 TensorFlow 中

以 PyTorch 为代表的框架生态主要有:

  • PyTorch
  • fast.ai:基于 PT 的高级模块框架
  • cafee:常用于计算机视觉领域,已集成到新版 PyTorch 中

静态图和动态图的区别

静态图

静态图把模型和数据分离开来,必须先用占位结点 (placeholder) 构建好计算图,然后才能将数据“喂入” (feed),在数据没有流入前是很难对网络进行调试的,这也是静态图的缺点。

动态图

可以随时捕获网络当前的状态,便于调试

两种框架的对比

TensorFlow PyTorch
计算图类型 静态图 动态图
学习成本 不易入门 易入门
执行效率 适合分布式 单机效率强大,缺点是CPU,GPU切换复杂
调试 不易调试 易调试
部署 易部署,友好支持多终端,支持分布式 还在完善中
生态 背靠 Google AI / Brain,生态强大 生态还在完善中

选择建议

个人一开始学习的是 TensorFlow 但是觉得有点生硬不太好掌握,后来转了 PyTorch,很快就入门,自己慢慢能用 PyTorch 做些实验,通过这些实验也慢慢对一些常用模型有了更深的理解。

后来的实习经历主要使用 TensorFlow,所以又把 TensorFlow 捡了起来,由于有了经验,再次学习 TensorFlow 时,发现它其实并没有想象中的那么难,后来发现 TensorFlow 更倾向于数学思维, PyTorch 更倾向于 工程思维

由于 PyTorch 的单机高效性,学习成本也比较低,近年来越来越多的论文用 PyTorch 实现;由于 TensorFlow 支持分布式,易部署所以业界用得比较多。

下文代码说明均使用 TensorFlow

常用深度学习模块原理及应用

CNN

CNN 主要由两个部分组成:卷积 (convolution) 和池化 (pooling) 两个部分。卷积主要用来提取特征;池化可以达到降维和降低模型复杂性抑制过拟合的目的,池化一般分为最大池化和平均池化,一般采用最大池化,因为最大池化具有平移不变性,这在一些任务中很重要。

卷积

假设有一个 \(5\times 5\) 的图像,使用一个 \(3\times 3\) 的卷积核 (filter) 进行卷积,想得到一个 \(3\times 3\) 的 特征表 (feature map),如下所示:

卷积计算有三个重要的量:

  • filter: 设置卷积核大小
  • padding: 设置输入的填充区域
  • stride: 设置卷积核窗口的滑动步数

一个 \(n\times n\) 的图像,经过大小为 \(f\times f\) 的卷积核,填充 (padding) 大小为 \(p\),步数 (stride) 为 \(s\),则得到的 feature map 为

\[(\left \lfloor \frac{n+2p-f}{s} \right \rfloor + 1, \left \lfloor \frac{n+2p-f}{s} \right \rfloor + 1)\]

一般我们想得到输入等于输出时需要设置 padding 值

一般我们想使得输出的尺寸比输入更低时,一般有两种方式:池化、调大 stride。通过调大 stride 的方式是改变卷积核的移动步长从而跳过一些像素

为了能捕获到不同的特征边缘信息,一般会使用多卷积核

池化

一般对卷积后得到的 feature map 进行降维,池化的作用:

  • 降低模型复杂性,抑制过拟合
  • 平移不变性(一般对于最大池化来说)

平移不变性 如果人们选择图像中的连续范围作为池化区域,并且只是池化相同(重复)的隐藏单元产生的特征,那么,这些池化单元就具有平移不变性 (translation invariant)。这就意味着即使图像经历了一个小的平移之后,依然会产生相同的 (池化的) 特征。在很多任务中 (例如物体检测、声音识别),我们都更希望得到具有平移不变性的特征,因为即使图像经过了平移,样例(图像)的标记仍然保持不变。

其他概念

感受野

feature map 中的输出对应回得到这个输出的输入区域,feature map 中的输出处于感受野的中心位置

空洞(膨胀)卷积

卷积核中只有一部分起作用

TensorFlow 的封装

TensorFlow 支持一维卷积 conv1d 、二维卷积 conv2d 和三维卷积 conv3d 操作,用得比较多得是一维和二维,具体的函数为:

一维卷积一般用于处理序列信息,常用于NLP任务,二维卷积一般用来处理三通道RGB的图片。

一般用一维卷积处理的任务可以通过扩充维度来使用二维卷积来处理,至于如何选择还得看具体的任务,NLP任务来说个人更偏向于使用 conv1d

无论是一维还是二维,卷积神经网络都具有相同的特点和相同的处理方法,区别在于滤波器的对数据的滑动方式不同

RNN及其变体

最简单的神经网络模型

没有上下文记忆,只保存当前状态

\[h = \sigma(Wx) \\\\ y = Vh\]

RNN

原理

含有上下文状态的神经网络模型

\[h_{t} = \sigma(Wx_{t} + Uh_{t-1}) \\\\ y_{t} = Vh_{t}\]

其中:

  • \(h_{t}\) 是 t 时刻的隐藏状态值
  • \(h_{t-1}\) 是 t-1 时刻的隐藏状态值
缺点

尽管 RNN 成功记忆了部分上下文信息,但存在一个很大的缺陷,那就是它很难记住长期的记忆,因为随着网络的加深网络会出现梯度弥散的问题,也就是很长的时刻前的输入,对现在的网络影响非常小,反向传播时那些梯度,也很难影响很早以前的输入

LSTM (Long Short-Term Memory) 长短时记忆

为了克服原始 RNN 的缺点,一些带门控单元的 RNN 被提出来,LSTM 是其中一个。

LSTM 的工作原理

\[\begin{align*} f_{t} & = \sigma(W_{f} [h_{t-1}, x_{t}] + b_{f}) \\\\ i_{t} & = \sigma(W_{i} [h_{t-1}, x_{t}] + b_{i}) \\\\ o_{t} & = \sigma(W_{o} [h_{t-1}, x_{t}] + b_{o}) \\\\ \widetilde{c_{t}} & = tanh(W_{c} [h_{t-1}, x_{t}] + b_{c}) \\\\ c_{t} & = f_{t}\odot c_{t-1} + i_{t} \odot \widetilde{c_{t}} \\\\ h_{t} & = o_{t} \odot tanh(c_{t}) \end{align*}\]

LSTM 有三个门:

  • input gate: 输入门
  • output gate: 输出门
  • forget gate: 遗忘门

有两个重要的 state (或memory):

  • 长期记忆 (long-term memory: ltm, 通常被称为cell state)
  • 工作记忆 (working memory: wm, 通常被称为hidden state)

LSTM 的迭代过程:

  • 选择性遗忘部分长期记忆:将记忆中不需要的记忆移除
  • 将当前时刻的一些信息加入到长期记忆中
    • 计算候选长期记忆
    • 选择函数
  • 从长期记忆中提取工作记忆
    • 计算候选的工作记忆
    • 选择函数

1. 选择性遗忘部分长期记忆

这部分主要由遗忘门来控制,通过遗忘门来决定哪些长期记忆能够被留下,在 t 时刻有:

\[remember_{t} = \sigma(W_{r} x_{t} + U_{r}wm_{t-1})\]

这里的 \(remember_{t}\) 是一个布尔序列,长度和上一时刻的长期记忆 \(ltm_{t-1}\) 相同,\(remember_{t}\) 中值为 1 代表保留 \(ltm_{t-1}\) 对应位置的值,0 代表遗忘。

上述的公式最终表示成:

\[f_{t} = \sigma(W_{f} [h_{t-1}, x_{t}] + b_{f})\]

则经过遗忘门后保留下来的记忆为:

\[old\_ltm_{t} = f_{t}\odot ltm_{t-1}\]

2. 将当前时刻的一些信息加入到长期记忆中

除了某些老的记忆需要保留,当前时刻的部分记忆也需要保留。

首先需要从上一时刻工作记忆及当前输入中计算全体候选记忆,在 t 时刻有:

\[ltm^{'}_{t} = tanh(W_{l}x_{t} + U_{l}wm_{t-1})\]

这里的 \(ltm^{'}\) 代表可能加入长期记忆的记忆序列,长度和 \(ltm_{t-1}\) 相同。

上述公式还可表示成:

\[\widetilde{c_{t}} = tanh(W_{c} [h_{t-1}, x_{t}] + b_{c})\]

有了候选记忆后,需要一个选择函数负责实际选择哪些记忆可以加入长期记忆,这个需要输入门来控制,在 t 时刻有:

\[save_{t} = \sigma(W_{s} x_{t} + U_{s}wm_{t-1})\]

上述公式还可表示成:

\[i_{t} = \sigma(W_{i} [h_{t-1}, x_{t}] + b_{i})\]

有了候选的长期记忆和选择函数后,我们就可以确定哪些记忆是要添加到长期记忆的。在 t 时刻:

\[new\_ltm_{t} = save_{t} \odot lsm^{'}\]

上面已经得到了老的长期记忆中在当前时刻要保留的部分 \(old\_ltm_{t}\) ,以及当前时刻的候选记忆中要保留的部分 \(new\_ltm_{t}\) ,因此 t 时刻最终确定的长期记忆为

\[ltm_{t} = old\_ltm_{t} + new\_ltm_{t}\]

上述公式还可表示成:

\[c_{t} = f_{t}\odot c_{t-1} + i_{t} \odot \widetilde{c_{t}}\]

3. 从长期记忆中提取工作记忆

长期记忆需要应用在当前的工作记忆中才有作用。

从上一个工作记忆和当前输入中选择,通过输出门来控制,在 \(t\) 时刻有

\[focus_{t} = \sigma(W_{f}x_{t} + U_{f}wm_{t-1})\]

上述公式可以表示成:

\[o_{t} = \sigma(W_{o} [h_{t-1}, x_{t}] + b_{o})\]

将长期记忆转换为工作记忆,在 t 时刻有

\[wm_{t}^{'} = tanh(ltm_{t-1})\]

上述公式还可表示成:

\[\widetilde{h}_{t} = tanh(c_{t})\]

有了选择函数和候选记忆则 t 时刻最终的工作记忆有

\[wm_{t} = focus_{t} \odot wm_{t}^{'}\]

上述公式还可表示成

\[\begin{align*} h_{t} & = o_{t} \odot \widetilde{h}_{t} \\\\ & = o_{t} \odot tanh(c_{t})\end{align*}\]

因此,LSTM 最终的结构为

LSTM 的变种

上述讲的 LSTM 是经典的结构,LSTM 有不少变种,用得较多的由两种:

  • Peephole LSTM
  • Gated Recurrent Unit (GRU)

1. Peephole LSTM

普通的 LSTM 的所有的门的决策全部都是由输入 \(x\) 和 工作记忆 \(h_{t-1}\) 决定,Peephole LSTM 改进了门的实现,让长期记忆 \(ltm_{t-1}\) 也参与门的决策。

2. Gated Recurrent Unit (GRU) 可以把 GRU 当作 LSTM 的高效压缩版,GRU 有两个门 reset gate 和 update gate,GRU使用了update gate替代了forget gate和input gate,而且将long-term memory 和 working memory 合并了,并做了一些细微的调整。

双向 RNN

双向的 RNN 同时考虑“过去”和“未来”的信息,可以更好的捕捉整体序列的语义信息、结构关系

Attention机制的原理以及实现

Attention 目标是从众多信息中选择出对当前任务目标更关键的信息,一般通过加权的方式进行。

Attention 在机器阅读理解任务中

计算 Attention 一般需要三个量 Q (query), K (key), V (value)

K 和 V 一般是对应的,在 NLP 中一般 K = V,对于 self-attention 有 Q = K = V

Attention 的计算主要分三步:

\[S = similarity(Q, K) \\\\ \alpha = softmax(S) \\\\ V^{'} = \alpha \cdot V\]

其中 \(similarity(*)\) 计算的是 Q 和 K 的相似度,该函数可由简单的点乘或者 \(cosine\) 函数来充当,也可使用多层感知机 (MLP) 来充当

NLP 领域常见的 Attention 模型

1. multihead-attention 这是由 Google 17年在 《Attention is all you need》 这篇论文提出的,区别于一般的 attention,multihead-attention 在计算时融合了不同子空间的信息,它的结构如下

2. BIDAF (Bidirectional Attention Flow) 16 年在论文《Bidirectional Attention Flow for Machine Comprehension》中提出, 是机器阅读理解中常用的 Attention 计算方式

使用 TensorFlow 实现 Self-Attention
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def attention(value,
initializer=tf.truncated_normal_initializer(stddev=0.1)):
Q = value
K = value
V = value

shape = V.get_shape().as_list()

W = tf.get_variable('attn_W',
shape=[shape[-1], shape[-1]],
initializer=initializer)

U = tf.get_variable('attn_U',
shape=[shape[-1], shape[-1]],
initializer=initializer)

P = tf.get_variable('attn_P',
shape=[shape[-1], 1],
initializer=initializer)

Q = tf.reshape(Q, [-1, shape[-1]])
K = tf.reshape(K, [-1, shape[-1]])

# calculate similarity
S = tf.multiply(
tf.matmul(Q, W),
tf.matmul(K, U))

# normalize
alpha = tf.nn.softmax(
tf.reshape(tf.matmul(S, P), [-1, shape[1], 1]), axis=1)

# apply attention
V_attn = tf.multiply(alpha, V)

return V_attn, alpha

Transformer

《Attention is all you need》还提出了基于 multihead-attention 的 transformer 结构,现在来看可以把 transformer 当成 CNN、RNN 同等重要的基本结构了。

近期热门的 GPT, BERT 都是基于 transformer 的,transformer 和 CNN 都可以支持并行计算,特征提取的能力也非常强,不过 transformer 也有缺点,它需要较大的参数量才能 work,所以当计算资源不是太强大时,transformer 往往不是很好的选择

常用的网络训练技巧

正则化的意义及使用方式

一般来说模型的风险可分为期望风险、经验风险和结构风险,损失函数是三者的基础,而讲到损失函数就不可避免的谈到成本函数、目标函数,所以借正则化这一节来区分这几个概念。

损失函数

损失函数一般是指对单个样本做的损失,用来衡量对单个样本的预测和真实值之间的差距,损失函数一般可记为 \(L(y, f(x))\) , \(f(x)\) 代表预测值

常见的损失函数有:

  • 0-1 损失函数: \(L(y, f(x)) = \begin{cases} 1 & \text{ , } y\neq f(x) \\\\ 0 & \text{ , } y = f(x) \end{cases}\)
  • 平方损失函数: \(L(y, f(x)) = (y- f(x))^{2}\)
  • 绝对损失函数: \(L(y, f(x)) = |y- f(x)|\)
  • 负对数似然: \(L(y, p(y|x)) = -log\ p(y|x)\)

经验风险 (empirical risk)

计算训练集中所有样本的平均损失值,用这个值去衡量模型的能力,这就是经验风险

\[R_{emp} (f) = \frac{1}{N} \sum_{i=1}^{N} L(y_{i}, f(x_{i}))\]

所谓经验风险最小化就是让上述式子最小化,经验风险越小说明模型f(X)对训练集的拟合程度越好

期望风险 (expected risk)

对于未知的样本数据的数量是不容易确定的,所以就没有办法和经验风险一样最小化所有样本损失函数的平均值,一般用期望来衡量未知样本的风险,称为期望风险。

假设X和Y服从联合分布 \(P(X,Y)\) ,那么期望风险就可以表示为:

\[R_{exp}(f) = E_{p}[L(Y, f(X))] = \int_{x\times y} L(y, f(x)) P(x, y) dxdy\]

期望风险表示的是全局的概念,表示的是决策函数对所有的样本预测能力的大小,而经验风险则是局部的概念,仅仅表示决策函数对训练数据集里样本的预测能力。因为期望风险比较难求,实际情况下用得比较多的还是经验风险。

结构风险 (structual risk)

如果只考虑经验风险那么模型很容易过拟合,导致模型的泛化能力差,这时候需要引入结构风险,结构风险是对经验风险和期望风险的折衷,结构风险一般可通过正则化来引入,这里点了本节的题,稍后会更详细介绍常见的正则化手段

\[R_{srm}(f) = \frac{1}{N} \sum_{i=1}^{N} L(y_{i}, f(x_{i})) + \lambda J(f)\]

成本函数

一般指数据集上总的损失,为了降低结构风险,往往加上正则项,所以成本函数可记为

\[J(W, b) = \frac{1}{N} \sum_{i=1}^{N} L(y_{i}, \hat{y_{i}})) + \lambda J(w)\]

目标函数

目标函数是一个非常广泛的名称,一般我们先确定一个“目标函数”再去优化它,不同的任务中“目标函数”可以是:

  • 最小化平方差错误成本函数 (CART, 线性回归等任务)
  • 最大化log-相似度或最小化信息熵损失函数
  • 最小化 hinge 损失函数 (SVM)

正则化

模型越复杂时包含的参数越多,当经验风险函数小到一定程度就出现了过拟合现象。

可以简单理解为模型的复杂程度是过拟合的重要条件,那么我们要想防止过拟合现象的方式,就要破坏这个必要条件,即降低决策函数的复杂度,所以我们一般通过正则化的方式去惩罚参数。

常用的正则化的方式有:

  • L1 正则
  • L2 正则
  • 随机失活 dropout
L1 正则, L2正则

L1 正则使用 L1 范数,倾向于把参数变稀疏;L2 正则使用 L2 范数,倾向于把参数变小;一般来说使用 L2 正则居多

范数 是具有“长度”概念的函数。在线性代数、泛函分析及相关的数学领域,是一个函数,其为向量空间内的所有向量赋予非零的正长度或大小

在图像上两者的区别

更一般的,在等高线图上有

可以想到加上了 L1正则后损失函数的等高线和 L1正则图像 的交点集中在 (0, 1) , (1, 0) 等稀疏点,因此 L1 正则倾向于将参数稀疏化,而 L2 正则倾向于将参数变小。

dropout

随机失活 dropout 一般用于神经网络模型,它的原理是对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。对于随机梯度下降来说,由于是随机丢弃,故而每一个 mini-batch 都能用不同的神经元训练不同的网络。

dropout 的实现方式非常简单,以下是用 numpy 来实现

1
2
3
4
5
6
def dropout(x, keep_prob=0.2)
shape = list(x.shape)
mask = np.random.rand(shape[0], shape[1]) < keep_prob
x = x * mask
output = x / keep_prob
return output

规范化的作用及常用的规范化方式

规范化一般指标准规范化,将数据按比例缩放,使之落入一个小的特定区间。规范化可以在一定程度上抵制异常值 (outliers) 的影响,使得网络更稳定,更容易找到最优解

未归一化的等高线图

归一化的等高线图

常见的归一化方式有:

  • Batch Normalization
  • Layer Normalization
  • Group Normalization
  • Instance Normalization

它们的原理都是

\[a^{*} = \frac{a-\mu }{\sigma} \\\\ a^{norm} = \eta a^{*} + \beta\]

其中 \(a^{*}\) 是标准归一化的结果, \(\eta\)\(\beta\) 是网络自动学习的参数,它们的作用是对 \(a^{*}\) 进行放缩移位

它们的区别在于对统计量 \(\mu\)\(\sigma\) 的求解,下图显示了各个 Normalization 使用不同的单元去计算两个统计量

使用经验

  • Batch Normalization: 一般用于 CNN 和 MLP,不用于 RNN,实践证明一般用在激活函数后
  • Layer Normalization: 可用于 RNN,其他情况下不如 Batch Normalization
  • Group Normalization: 一般用于 CNN
  • Instance Normalization: 不常用

常见的激活函数及选择

激活函数是非线性的,一般是全局可导的,常见的激活函数有

  • sigmoid
  • tanh
  • relu (Rectified Linear Unit) 线性修正单元
    • leaky_relu
    • gelu
    • elu

sigmoid

\[\sigma(z) = \frac{1}{1+e^{-z}}\]

sigmoid 一般用于二分类任务

tanh

\[tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}}\]

tanh 一般和 RNN 结合使用

relu

\[ReLU(z) = max(0, z) = \begin{cases} 0 & \text{ , } x\leqslant 0 \\\\ z & \text{ , } x > 0 \end{cases}\]

对比三者的图像可知,sigmoid 和 tanh 只有很小部分区域的梯度变化比较明显,大部分区域梯度几乎没有变化,正是这个原因很容易导致梯度弥散问题。而 ReLU 没有这个缺点,所以 ReLU 广泛用做隐层的激活函数

常见的优化器及选择

深度学习中一般使用梯度下降算法来优化,常用的优化器有

  • 随机梯度下降 (Stochastic Gradient Decent, SGD)
  • 动量梯度下降 (Gradient Descent with Momentum)
  • 均方根支梯度下降 (Root Mean Square Prop, RMSProp)
  • 自适应矩估计 (Adaptive Moment Estimation, Adam)

SGD

每次通过一个样本来迭代更新

\[\begin{align*} J(w, b) = L (\hat{y^{(i)}}, y^{(i)}) + \frac{\lambda }{2}\sum \left \| w \right \|_{F}^{2} \\\\ w_{j} := w_{j} - \alpha \frac{\partial J(w, b)}{\partial w_{j}} \\\\ b_{j} := b_{j} - \alpha \frac{\partial J(w, b)}{\partial b_{j}} \end{align*}\]

Momentum

计算梯度的指数加权平均数,并利用该值来更新参数值

\[\begin{align*} & \upsilon_{dw} = \beta \upsilon_{dw} + (1-\beta) dw \\\\ & \upsilon_{db} = \beta \upsilon_{db} + (1-\beta) db \\\\ & w := w - \alpha \upsilon_{dw} \\\\ & b := b - \alpha \upsilon_{db} \end{align*}\]

SGD 在局部沟壑中很容易发生振荡,所以在这种情况下下降速度会很慢,而动量能在一定程度上抑制这种震荡,使得SGD的下降更平稳

RMSProp

在梯度进行指数加权平均的基础上引入了平方和平方根

\[\begin{align*} & S_{dw} = \beta S_{dw} + (1-\beta) dw^{2} \\\\ & S_{db} = \beta S_{db} + (1-\beta) db^{2} \\\\ & w := w - \alpha \frac{dw}{\sqrt{S_{dw} + \epsilon }} \\\\ & b := b - \alpha \frac{dw}{\sqrt{S_{db} + \epsilon }} \\\\ \tag{10} \end{align*}\]

\(\epsilon\) 一般值很小,主要是用来提高数值稳定性,防止分母过小

特点:\(dw\)\(db\) 较大时,\(dw^{2}\)\(db^{2}\) 也会较大,因此 \(S_{dw}\) \(S_{db}\) 也是较大的,最终使得 \(\frac{dw}{\sqrt{S_{dw} + \epsilon}}\) \(\frac{db}{\sqrt{S_{db} + \epsilon}}\) 较小,这也减少了振荡

Adam

可以认为是 MomentumRMSProp 的结合

\[\begin{align*} & \upsilon_{dw} = \beta_{1} \upsilon_{dw} + (1-\beta _{1}) dw, \upsilon _{db} = \beta_{1} \upsilon_{db} + (1-\beta _{1}) db \\\\ & S_{dw} = \beta_{2} S_{dw} + (1-\beta _{2}) dw^{2}, S_{db} = \beta_{2} S_{db} + (1-\beta _{2}) db^{2} \\\\ & \upsilon_{dw}^{correct} = \frac{\upsilon _{dw}}{1-\beta_{1}^{t}}, \upsilon_{db}^{correct} = \frac{\upsilon_{db}}{1-\beta_{1}^{t}} \\\\ & S_{dw}^{correct} = \frac{S_{dw}}{1-\beta_{2}^{t}}, S_{db}^{correct} = \frac{S_{db}}{1-\beta_{2}^{t}} \\\\ & w := w - \alpha \frac{\upsilon_{dw}^{correct}}{\sqrt{S_{dw}^{correct}} + \epsilon} \\\\ & b := b - \alpha \frac{\upsilon_{db}^{correct}}{\sqrt{S_{db}^{correct}} + \epsilon} \\\\ \tag{11} \end{align*}\]

\(\beta _{1}\)为第一阶矩,\(\beta _{2}\) 为第二阶矩

使用经验

一般来说 Adam, Adamax, Adadelta, Adagrad, AdamW 等优化算法会更快更暴力的收敛,但是往往不易找到全局最优,经验上来说一般使用动量梯度下降更容易找到全局最优,不过收敛速度慢

mask 的作用

mask 是网络训练的一种常用的 trick, 在 NLP 任务中训练数据通常以 mini-batch 的方式准备,一般来说 mini-batch 中的数据是长短不一的,所以往往需要对数据进行 padding,使得 mini-batch 长短一致。

为了减弱 padding 部分的影响,一般对 mini-batch 进行 mask, 然后减小 padding 部分的权值

对于抽取性的任务,如:机器阅读理解,文本摘要,使用 mask 一般会使模型效果有略微提升

TensorFlow 实现 mask

1
2
3
4
5
6
7
8
9
batch_seq = tf.placeholder(tf.int32, 
[batch_size, None],
name="sequence")
# TODO ...

mask_value = 1e-12
seq_mask = tf.cast(
tf.cast(batch_seq, tf.bool), tf.float32)
masked = batch_seq * seq_mask + mask_value * (1. - seq_mask)

模型效果提升偏方

迁移学习

在 NLP 中迁移学习的思想一般是使用预训练词向量(如 word2vec、fasttext、glove)或者使用预训练语言模型 (ELMo、GPT、BERT)

预训练词向量

由于向量表示是低维稠密的分布式向量,所以可以进行语义计算,但只是对词做了简单的空间映射,也就是说同一个词的向量表示是相同的。使用的话,一般将预训练的词向量权值表替换掉随机初始化的权值表,在 TensorFlow 实现可以是:

1
2
3
4
5
6
7
pretrained_word_matrix = tf.get_variable(
'word_embeddings',
shape=[vocab_size, embedding_size],
initializer=tf.constant_initializer(
pretrained_word_embeddings),
trainable=True
)
预训练语言模型

今年 (2018年) 预训练语言模型陆陆续续被提出, ELMo、GPT、BERT 是之中的三个代表,其中 BERT 的推出更是 (绝对地) 刷新了多项 NLP 任务 (一般来说 Google 一出手,其他人就没法做了)。

预训练语言模型和预训练词向量的区别在于可以认为预训练语言模型训练的是模型 \(f(x)\) ,预训练语言模型可以学到上下文信息,根据输入上下文的不同可以动态的得到不同的词向量。

比如:

1
2
我喜欢吃 苹果 
我喜欢用 苹果 电脑

预训练词向量中两个句子中的苹果的词向量是相同的,而预训练语言模型得到的苹果是不同的,因为两个句子的上下文是不同的

关于 ELMo、GPT、BERT 的区别在下一节再做简要介绍

鉴别学习 (identity learning)

其实这一块我并没有深入的研究,只是在实习中用到了相关的知识。

不同的 NLP 任务往往有不同的指标,而指标往往是评估模型的重要因素,我们往往希望指标朝着期望的方向发展,所以我们可以把指标加入到损失函数中让优化器也一同优化指标,当然怎么加、如何控制是一门学问,这个还得凭自己经验来添加。

模型融合 (ensemble)

融合是一门大学问,一般来说可分为:

  • 单模型融合
  • 多模型融合

单模型融合一般通过 k-fold 交叉验证的方式进行

来自:《花式自然语言处理》,苏剑林,中山大学

多模型融合一般也是通过 k-fold 交叉验证的方式:假如有 \(m\) 种不同的模型,每种模型做 \(n\) 划分交叉验证,可以得到 \(m \times n\) 个不同的模型,通过一个新模型来融合这 \(m\times n\) 个模型

来自:《花式自然语言处理》,苏剑林,中山大学

模型蒸馏

模型蒸馏是 G.Hinton 在 2015 年提出来的 (来自论文:《Distilling the Knowledge in a Neural Network》), 可以把它当作一种模型压缩方法、迁移学习、模型融合方法。

它的主要思想是:通过训练一个(或多个)复杂的网络模型 (teacher),然后用这个复杂模型去调教一个简单网络模型 (student) ,通过训练,简单模型会慢慢学到复杂模型的知识,甚至青出于蓝胜于蓝。

一般通过对 softmax 函数添加温度(temperature, T) 来实现

\[q_{i} = \frac{exp(z_{i} / T)}{\sum_{j} exp(z_{j}/T)}\]

通过温度控制会使得 softmax 更稳定更,最终有可能超过原模型。

深度学习与NLP

word2vec vs fasttext

两者都是得到低维稠密的分布式向量的方法,分布式向量表示具有语义计算、语义推理的能力

word2vec

word2vec 是一种三层结构:输入层,投射层,输出层。

输入层

输入层是输入词的 one-hot 编码。

深度学习框架处理 NLP 任务时,输入一般采用压缩 one-hot编码(字典表示),深度学习框架会自动转化为 one-hot 矩阵

投射层

投射层即要训练的 embedding 层,训练完毕后我们可以用这一层得到的权值作为 word embedding 去训练模型。

输出层

输出层是 softmax 分类器,word2vec 采用了树结构表示的 hierarchical softmax 和构造正负样本的 negative sampling 方式去优化输出层,使得输出层的 softmax 函数不必轮询每个词,大大减小了计算路径的长度,从而保证 word2vec 的高效性。

语料构造方式

虽然表面上 word2vec 是无监督的,但实际的训练过程是有监督的,它的有监督语料主要受两种模型影响

  • CBOW (Continuous Bag of Words)
  • Skip-Gram

fasttext

fasttext 和 word2vec 原理类似,训练方式类似,区别在于:

  • fasttext 需要提供有监督的语料
  • fasttext 使用了 n-gram 特征

n-gram 特征的主要作用: 通过共享权值,改善生成词向量的质量

如何给网络添加特征

特征有很多种,词、字、词性、位置信息、偏旁部首、n-gram等,一般来说添加适量的特征可以提升模型效果,特别在小数据量的情况下。

本人经验,添加特征的方式一般有两种:

  • embedding: 将不同特征的 embedding 拼接作为最终的词嵌入向量表示
  • attention: 把特征当作 attention 里的 K (Key)

ELMo、GPT、BERT

这三个模型在今年陆陆续续地刷新了各大 NLP 榜单,总结了一下,它们的都有一个特点:层网络 + 规模训练语料 + 微调即可迁移到其他下游任务

  • ELMo: 来自 Allen Institue, 论文 《Deep contextualized word representations》
  • GPT: 来自 OpenAI , 论文 《Improving Language Understanding by Generative Pre-Training》
  • BERT: 来自 Google , 论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》

ELMo 一般采用双向的 LSTM 或者 CNN 来训练,而 GPT、BERT 采用的是 transformer

Word2vec、ELMo、GPT、BERT四者的关系

区别于 GPT , BERT的 transformer 之所以是双向的,是因为 BERT 采用的是 transformer 的 encoder 部分进行编码,而这部分是双向的,而 GPT 采用了 transformer 的 decoder 这部分是单向的,BERT 论文中关于单向和双向的讨论如下:

We note that in the literature the bidirectional Transformer is often referred to as a “Transformer encoder” while the left-context-only version is referred to as a “Transformer decoder” since it can be used for text generation.

详细请查阅原论文

Tensorflow 实战:以DPCNN为例讲述如何实现一篇论文

本人经验,实现论文常用步骤

  • 首要条件是读懂论文,知道论文提出的模型采用了哪些结构,最好是看到公式心里就出现代码(比如说看到一堆 lstm 的公式就想起 lstm 相关的代码,这样可能一行代码就可以解决论文中一堆公式了)
  • 语料如何构造
  • 使用了什么 Attention 机制,以及如何计算 Attention
  • 模型实现的 tricks 及调参说明 (最终能不能复现这部分很重要)

本节以论文 DPCNN: Deep Pyramid Convolutional Neural Networks for Text Categorization 为例讲述如何用 TensorFlow 实现一篇论文

1. 读懂论文

DPCNN 是腾讯 AI Lab提出的,被ACL 2017 收录,主要用于文本分类,是一种基于 CNN 的浅层(相比大多数残差网络来说有点浅)残差网络结构

2. 实现模型结构

大多数论文都会给出模型的网络结构,以及网络结构的说明,要实现论文这一块内容非常重要。

DPCNN 的模型结构为:

可以看到这是一个残差结构,而且网络结构中的模块以卷积为主,那接下来的工作就是搭乐高积木的过程了

残差 = 预测值 - 观测值 神经网络在反向传播中会不断的更新梯度,当网络层数加深时,梯度在逐层传播的过程中会逐渐减弱,最后导致对先前的网络权重(由于采用了链式求导法则,所以必定要更新先前网络的权重)更新困难。在残差网络中,会在当前时刻的网络层中加入先前添加的观测连接(也称short connections)这样直接为当前层网络提供了一个直接连接先前层网络的通道使得可以直接更新先前网络的参数,因此缓解了梯度减小的问题

1. 首先定义好输入层的变量

1
2
3
4
5
6
7
8
9
10
# 输入的 mini-batch 数据
self.inputs = tf.placeholder(tf.int32,
[None, self.config.max_sent_len],
name="inputs")

# 真实标签
self.labels = tf.placeholder(tf.int32,
[None],
name="label")
# TODO: others

2. 词嵌入层 将压缩的 ont-hot 编码 (字典形式) 转为分布式编码

1
2
3
4
word_matrix = tf.get_variable('word_embeddings',
shape=[vocab_size(), embedding_size],
trainable=False)
self.input_embedding = tf.nn.embedding_lookup(word_matrix, self.inputs)

3. 构造网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
inputs = self.input_embedding

conv1 = tf.layers.conv1d(inputs,
250, # filters
3, # kernel size
activation=tf.nn.relu,
name='conv1')

conv2 = tf.layers.conv1d(conv1,
250, # filters
3, # kernel size
activation=tf.nn.relu,
name='conv2')

# short-connection (引入观测点)
inputs = conv2 + inputs

# Repeat
for i in range(self.config.num_blocks):
seq_len = inputs.get_shape()[1]

# Downsampling: pooling / 2
poolings = tf.transpose(
tf.nn.top_k(tf.transpose(inputs, [0,2,1]), k=seq_len // 2)[0], [0, 2, 1])

conv1 = tf.layers.conv1d(poolings,
250, # filters
3, # kernel size
activation=tf.nn.relu,
name='conv1-%d' % i)

conv2 = tf.layers.conv1d(conv1,
250, # filters
3, # kernel size
activation=tf.nn.relu,
name='conv2-%d' % i)

# short-connection (引入观测点)
inputs = conv2 + poolings

# pooling
self.outputs = tf.reduce_max(inputs, axis=1)

3. 完善模型,加入 tricks, 调参数

本节后记

本节涉及到的完整代码已经放在 github / clfzoo 上,欢迎给这个 repo 添砖加瓦

代码能力其实就一句话:无他,但手熟尔。

一定要向别人学习,学习阶段要多动手自己造轮子。

写在最后

本人才疏学浅、思维深度浅,所以不免有所遗漏和错误的地方,欢迎大家指正,多交流!

Refrence

sean lee wechat
欢迎关注我的公众号!
感谢支持!