计算机视觉-神经网络的训练

计算机视觉-神经网络的训练

引入

我们在 卷积神经网络理论 这篇博客中粗略学习了卷积神经网络的相关知识。

在那篇博客里,我们用举了很多例子,比如说手写识别,图像分类。但其实CNN还有其他作用,比如:

  • 风格迁移

我们输入一张内容图片,再输入一张风格图片,通过最小化内容的损失和风格的损失来获取一张既保留前一张图片内容与后一张图片风格的图片。

https://dreamscopeapp.com/ 中,就是利用CNN这项技术来实现风格迁移的。

  • Alpha Go

Alpha Go的原理就是提取出棋盘上的概率特征,然后去判断落子的位置

  • 看图说话

如果图片和文本联系起来,可以实现看图说话的功能:每个词出来以后,预测下一个词是什么

激活函数

现在我们来讲一些神经网络训练中的技巧。首先就是如何选择激活函数:

我们知道比较有代表性的激活函数有Sigmoid,Relu,tanh,那么它们各自的优缺点是什么呢?

Sigmoid

我们观察到Sigmoid函数,y在x的绝对值大于4的时候,就非常非常接近于1了,而且基本维持不变。

因此,Sigmoid的优点是:

  • 可以把数据压缩到0和1之间,每层数据的变化不是很大,如果变化太大会导致训练的不稳定,从而参数的梯度会很大,训练的代价高。
  • Sigmoid在任意位置均可导

Sigmoid的缺点是

  • 饱和输出
  • 非零均值,均值为0.5
  • 属于幂函数计算代价高

我们主要来关注前两个缺点:

饱和输出

饱和输出的意思是,当x稍微大一点或小一点的时候,$\sigma(x)$ 就会无限接近1或者0了。从而导致sigmoid的局部梯度接近于0,造成回传梯度消失,参数无法更新

非零均值

首先,对于Sigmoid函数,$\sigma_i$都是正数。那么对于回传来的梯度,我们有公式:

上游传回来的梯度如果是正的,那么每个$w_i$的梯度都是正的;上游传回来的梯度如果是负的,那么每个$w_i$的梯度都是负的。这就造成每一层的weight要么全部变小,要么全部变大,梯度呈“之字形”更新,虽然最后也能收敛到最优解,但收敛很慢

minibatch可以减轻这种效应

tanh

优点:

  • 将数值压缩到(-1,1)之间
  • 零均值
  • 曲线平滑,便于求导

缺点:

  • 容易饱和输出
  • exp() 幂函数计算复杂度很高

所以说 $\tanh$ 和 $\text{sigmoid}$ 的缺点是相同的

Relu

其优点有很多:

  1. 在正区间是不会饱和的
  2. 计算复杂度极低
  3. 由前两条可以得到收敛速度比Sigmoid和tanh快

当然,也存在一些缺点

  1. 不是零均值,也会存在一些 Zig-Zag的问题
  2. 不会压缩数据,因此数据幅度会随着网络加深不断增大
  3. 神经元坏死

神经元坏死

由于参数初始化或者学习率设置不当,会导致某些神经元的输入永远是负数,那么经过Relu,梯度为0,导致相应的参数永远不会更新。这就是Dead ReLU

采用合适的参数初始化和调整学习率可以缓解这种现象

Leaky ReLU

Leaky ReLU和Relu很像,只不过在小于0的时候也有一个很小的斜率(取决于超参数的大小)。这样可以继承ReLU的优点,又规避了ReLU神经元坏死的缺陷

优点:

  • 不会造成饱和 计算复杂率低
  • 收敛速度比Sigmoid和tanh快
  • 近似零均值
  • 解决ReLU的神经元坏死问题

缺点:

  • 数值幅度不断增大
  • 实际表现不一定会比ReLU好

最后一点在科学研究中很常见,就是提出了能规避缺点的好方法,但却并不能得到更好的效果

ELU(Exponential Linear Units)

我们看到ELU和Leaky ReLU很像,只是一个是线性函数,一个是幂函数

优点:

  • 不容易造成饱和
  • 收敛速度比Sigmoid和tanh快
  • 近似零均值
  • 解决了ReLU的神经元坏死问题

缺点:

  • 存在幂函数,exp()计算复杂度高
  • 表现不一定比ReLU好

Maxout

上面的输入层有8个神经元,maxout以后只有四个输出神经元,因为两两组合选出最大的那个值作为输出

推广得到:

maxout讲的不是很多,其原理就是每一个输出神经元的输出都会和上一层的输入神经元做计算,最后取最大值作为输出。

实际搭建模型的时候:

  • 首选ReLU,但是要注意初始化和学习率的设置
  • 不要使用Sigmoid
  • 可以使用tanh,不过效果来讲一般
  • 可以多尝试一点其他激活函数

数据预处理

调整图片大小

一般来说,将图像裁剪为大小一致的正方形。可以通过downscale和upscale来调整大小。

可以使用Pillow的crop()resize()方法

图像序列化

可以使用pickle模块将图片转化为像素值数组,并附上相应标签

零均值化

目标:将原始像素值从[0,255]调整为$[-128,127]$ .

方法:

  • 计算所有图像的平均,得到mean image,mean image和原始图像的大小应保持一致

  • 然后将每个图像都减去mean image

如果输入数据不是零均值的话,会产生什么影响?造成这一层参数的梯度,要么都是正的,要么都是负的,导致Zig-Zag效应

此外,还有一些零均值化的方法,比如:

  • 每个channel 减去各自的平均。 比如 VCGNet
  • 每个channel 减去各自的平均,然后除以std(标准化). 比如 ResNet

标准化

目标:将数值压缩到一个较小的区间

好处:

  • 减小损失函数对权重参数变化的敏感度
  • 方便优化参数

在实际搭建模型的时候,必做的三个步骤是:

  1. 调整图像大小
  2. 图像序列化
  3. 零均质化

等每一层卷积之后,再去做标准化,一般先不做。

权重参数的初始化

初始化参数的好坏对网络的训练有着非常重要的意义。

由链式规则可知:

那么当参数初始化过小的话,每一层回传值都$\approx 0$

  • 会导致回传梯度快速接近0,梯度消失。靠近输入层的梯度无法更新

如果参数初始化过大(>1)

  • 每一层都会乘以一个大于1的数字,会导致回传梯度快速增大,梯度爆炸。靠近输入的梯度会更新太快. 不知道什么时候才能找到最优解

下面这张图中,纵轴代表更新速度,越上面代表更新越快。layer4是靠近输出层的,layer1是靠近输入层的。我们看到靠近输入层的layer1更新速度非常小。

下面接好一些参数初始化方法

全部初始化为0

如果全部初始化为0,会导致:

  1. 每一层的神经元输出完全一样
  2. 每一层的参数梯度完全一样(参数的梯度和输入有关)
  3. 每一层的参数永远相同
  4. 学习很缓慢,无法学习数据特征

完全随机初始化

若把参数设定为零均值、方差较小的正态分布随机数

1
2
3
4
5
6
7
dims = [4096]*7
hs = []
x = np.random.randn(16,dims[0])
for Din,Dout in zip(dims[:-1],dims[1:]):
W = 0.01*np.random.randn(Din,Dout)
x = np.tanh(x.dot(W))
hs.append(x)

在这里我们用 tanh当做激活函数,初始化的数据也比较小。主要集中在0附近,因此,会导致下面这种情况,就是随着网络的一层层加深,经过激活函数 tanh 的时候会越来越塌陷,所有参数越来越靠近0,被激活的越来越少。

从而导致,越靠近输出层,梯度 $\frac{\partial L}{\partial w}$ 接近于0,靠近输出层的w的无法更新

若把参数设定为零均值,方差较大的正态分布随机数

1
2
3
4
5
6
7
dims = [4096]*7
hs = []
x = np.random.randn(16,dims[0])
for Din,Dout in zip(dims[:-1],dims[1:]):
W = 0.1*np.random.randn(Din,Dout)
x = np.tanh(x.dot(W))
hs.append(x)

那么这时候,经过tanh激活函数后,随着网络越深,输入值矩阵会越来越大,和权重相乘之后会无限接近于-1或者无限接近于1。从而导致激活越来越饱和。

这样一来,激活门的局部梯度就接近于0,根据tanh的导数图像可知,由于激活后的tanh都集中在-1和1,导致回传的梯度会消失

所以不管怎么样,都会塌陷或饱和。

Xavier 初始化

最理想的状态是,梯度既不会消失,也不会饱和,尽可能保持y和x的分布保持一致。现在来介绍Xavier初始化:

其方法就是在正态分布的随机数基础上除以$\sqrt n$ : $std = 1/\sqrt{\text{Din}}$

卷积层Din的大小怎么算?$\text{Din} = F^2\times k$ ,其中 F为filter大小,输入K为信道数

1
2
3
4
5
6
7
dims = [4096]*7
hs = []
x = np.random.randn(16,dims[0])
for Din,Dout in zip(dims[:-1],dims[1:]):
W = np.random.randn(Din,Dout) / np.sqrt(Din)
x = np.tanh(x.dot(W))
hs.append(x)

会有一定的塌陷,但是塌陷的速度明显减慢了

如果将激活函数替换为ReLU的话:

1
2
3
4
5
6
7
dims = [4096]*7
hs = []
x = np.random.randn(16,dims[0])
for Din,Dout in zip(dims[:-1],dims[1:]):
W = np.random.randn(Din,Dout) / np.sqrt(Din)
x = np.maximum(0,x.dot(w))
hs.append(x)

He初始化

He初始化是在Xavier上的基础上做了一些微小的改动,他觉得Xavier的更新还是太快了,因此除了一个更小的值.结果效果非常好:

1
2
3
4
5
6
7
dims = [4096]*7
hs = []
x = np.random.randn(16,dims[0])
for Din,Dout in zip(dims[:-1],dims[1:]):
W = np.random.randn(Din,Dout)/np.sqrt(Din/2) # 相当于 *np.sqrt(2/Din)
x = np.maximum(0,x.dot(w))
hs.append(x)

在实际搭建模型的时候,优先使用ReLU+He进行初始化

Batch Normaliziation

最后一部分我们来聚焦标准化。之前我们说,在数据预处理的时候先不要做数据标准化,这是因为有人发明了在层与层之间做标准化的方法,效果更好。

其中一个效果较好的方法就是 Batch Normaliziation

提出这个方法的内驱力是:

  • 如果不做标准化,输入的变化很大,导致参数也不停地变化,进而导致内部输出的分布不停变化。损失函数很难收敛
  • 不做标准化,会导致数很大或者很小,进而导致激活容易饱和,或者激活趋近于0
  • 总的来说,不做标准化会导致神经网络难以收敛

因为这种方法是通过对Minibatch标准化,因此我们称其为 Batch Normalization.

其原理就是对特征的每一维单独做normalization:

详解:

然后,我们要引入参数$\gamma$和$\beta$ ,也就是引入一个标准化的程度,否则如果强行标准化到$N(0,1)$可能会导致某些特征发生改变

在推理时,可能只有一个或者几个样本,因此无法计算稳定的、有效地计算$\mu$和$\sigma^2$ .我们希望使用固定的$\mu,\sigma^2$ ,否则,整个模型会一直变化。

关于$\mu$和$\sigma^2$ 怎么更新,Moving Average技巧,momentum是一个超参,一般设为0.99或者0.999,然后让mean和var每次都会更新,但是更新幅度较小。最后获得一个整体的均值

1
2
running_mean = momentum * running_mean + (1-momentum) * sample_mean
running_var = momentum * running_var + (1-momentum) * sample_var

到最后我们会发现,变量只有x,其余的都是超参数,可以将以下两式融合成 线性结构:

矩阵运算视角

全连接层

对于输入x , 是 $N\times D$ 的矩阵

Batch Normalization就是保留输入的维度(即D),对每一维里面所有的元素进行标准化

卷积层

对于卷积层,输入为 $\text{N}\times \text{C}\times \text{H}\times W$

Batch Normalization就是保留输入的信道,是对每个信道里面的所有元素进行标准化。最后保留C个均值、C个方差

BN放在那里呢?

BN是放在卷积/全连接层和激活函数之间的一层。

BN的注意点

  • 优化了梯度流,是深度网络训练起来更加容易
  • 可以使用较大的Learning Rate,加速收敛
  • 受权重参数初始化影响较小
  • 在训练过程中起到正则化的作用
  • 推理时和FC/Conv层融合,几乎不增加开销
  • 注意训练和推理BN的具体实现是不同的

Normalization的变化

有时候,Batch Norm 在神经网络中的作用不那么突出,因此提出了一些变形。其原理和思想都和Batch Norm一样,只是参与归一化的元素不同

  • Batch Normalization:对信道中的所有元素进行Normalization

  • Layer Normalization :C代表每个信道,N代表每张图片。Layer Normalization就是对Batch中的每一张图片进行Normalization

  • Instance Normalization:每一张图片中的每一个信道做Normalization,更细致一些

  • Group Normalization: 取一张 图片中的部分信道做Normalization,介于Layer和Instance之间

优化方法的演进

前面我们讲了,通过反向传播来得到不同层的梯度,得到了梯度怎么拿来更新模型中的参数呢?这边又涉及到了不同的技巧,我们来学习一些

SGD

SGD的全称是 Stochastic Gradient Descent,即随机梯度下降。

标准的随机梯度下降不是一个batch一个batch来做的,而是每次使用一个数据(一张图片)来计算loss。得到了损失函数,我们马上来更新参数。

伪代码如下:

1
2
3
while True:
weights grad = evaluate gradient(loss fun,data,weights) #根据当前图像计算梯度
weights += -step size * weights grad

SGD的迭代次数多,更新速度快,但也有其不可规避的缺点,比如:

zig-zag问题

假设在二维空间中,损失函数对参数$w_1$ 非常敏感,但是对$w_2$不太敏感。

那么当w1稍微改动一下,损失函数就变化很大;w2可能要改动很多损失函数才能变化。

下图每个圈代表相同的损失函数的值,我们发现由于损失函数对参数的敏感程度的不同,会导致Zig-Zag的问题

当然,用非零均值的激活函数也会造成Zig-zag的问题。

minibatch的噪声问题

我们可以用 mini-batch 则来效缓解Zig-zag的问题。但同时也会引入一些噪声。

局部最优和鞍点

SGD是每次用当前的参数减掉计算得到的梯度来得到下一轮的参数的。因此,如果当损失函数陷入极小值点或者说进入鞍点的时候,参数会很难更新。如下图:

当落入极小值点的时候,梯度是接近于0的,此时更新就很难移动了。稍微动一点点的话,梯度就变成反方向的了,就好像U形管中的小球一样。

在高位空间中,鞍点是更常见的,此时,某些方向loss会变大,某些方向loss会变小。

Momentum

利用SGD+Momentum,我们可以解决SGD带来的缺点。其更新原理如下:

1
2
3
4
5
vx=0
while True:
dx = compute_gradient(x)
vx = rho*vx+dx
x -= leaning_rate * vx

在SGD中,我们把梯度看做是一个静态的量,每次求得以后用原参数减去即可。但是在SGD+Momentum中,就相当于给梯度加上一个”速度“——下一时刻的速度,等于当前时刻的速度乘以一个衰减率$\rho$, 再加上求得的梯度。然后,用当前的权重减掉速度来更新权重。

等于说,现在我改变的是速度的大小,速度是由梯度累加起来的一个量。梯度$\nabla f(x)d(x)$越大,增量也就越大, 这样就避免了一个速度为0的情况,因此权重是一直可以更新的。

这种方法,在当前速度和梯度方向相同的时候,会让权重减小较多的值,相当于踩了一个刹车,来让我们不那么容易地错过最优点。如果当前处于极小值点/鞍点,仍然是有一个速度存在的(可能为负数),借助于这个速度,可以跳过这个极小值旁边的坡。如上图所示

在可视化图中,我们发现 SGD+Momentum 在最后绕过了最优解,最后又绕回来了。这就是速度的直观表现,一开始遇到最优点但是刹不住车了,后来再一步一步减小自己的速度,回到最优点

Nesterov Momentum

对于SGD+Momentum,Nesterov提出了一种改进方法,如下图所示:

Nesterov 的依据是,下一层速度的更新,不要依据当前点的梯度,而要依靠下一个点的梯度。这样,如果下一个点的梯度非常陡峭,如果和速度同方向,说明正在下坡,$v{t+1}$变小,因此可以慢一点;如果和速度反方向,说明在爬坡,$v{t+1}$增大,需要加速。

计算公式:

因此,我们只要计算$\tilde x$即可

1
2
3
4
dx = compute_gradient(x)
old_v = v
v = rho*v-leaning_rate*dx
x += -rho*old_v+(1+rho)*v

AdaGrad

对于SGD、以及Momentum,我们发现对某一层所有点的梯度更新都是一样的。但是实际情况下,某些方向对参数的更新是不明感的,而有些方向很敏感。因此我们要对敏感方向更新的快一些,而对不敏感的方向慢一点。

由此,我们来学习AdaGrad,这个优化策略的思想是:

对参数的每一维,都累计当前梯度的平方,这样,梯度越大,这个方向累计的数值就越大。

接下来,用这个累计梯度平方根去计算$\alpha$, 累计梯度越大,$\alpha_t$就越小;累计梯度小的,$\alpha_t$就越大

我们把$\alpha$ 作为参数更新的学习率,使得每个维度的$\alpha$ 不同. 这样,对于各个方向的参数更新都会平滑一些,不会导致某些方向参数更新快,某些方向参数更新慢的这种Zig-Zag情况

代码:

1
2
3
4
5
6
grad_squared = 0
while True:
dx = compute_gradient(x)
grad_squared += dx*dx
x -= learning_rate * dx /(np.sqrt(grad_quared)+1e-7)
# 1e-7是超参,用以防止除零

同时,Adagrad也有其局限性,因为在一段时间之后,在梯度更新很快的方向,$\sqrt{r_t}$很大,这个方向学习率可能会趋近于0,有可能导致在鞍点附近的参数无法更新

RMSProp(Leakey AdaGrad)

为了解决某些方向累计梯度过大,我们对AdaGrad进行更新:对梯度平方做了一个很细小的改变(momentum):

1
2
3
4
5
grad_squared = 0
while True:
dx = compute_gradient(x)
grad_squared += decay_rate*grad_squared+(1-decay_rate)*dx*dx
x -= learning_rate * dx /(np.sqrt(grad_quared)+1e-7)

这样给累加的梯度乘以一个衰减率(0.9或者0.99), 当某一个方向梯度更新得快的时候,就不会导致累计梯度增加过快。

从图中我们可以看出,RMSProp相对与Momentum方法,它不容易越过最优点,而是以一种缓慢接近的方式达到最优点

Adam

Adam的全称是 Adaptive Moment estimation

这种方法就是将momentum和Adagrad的思路结合起来

首先,first_moment = beta1 * first_moment + (1-beta1)*dx, 也就是momentum的计算。给梯度加上一个moving average。

其次,second_moment = beta2 * second_moment + (1-beta2)*dx*dx 也就是Leakey AdaGrad的做法,

最后,将这两种方法结合起来,即:x -= leaning_rate * first_unbias / (np.sqrt(second_unbias)+1e-7)

注意,如果我们直接把first_moment和second_moment引用到更新式子中去的话,可能会导致初期的学习率过大. 因此,我们需要用bias correction来防止初期学习训练率过大的情况——让它们除以一个小于1的值,用来修正过大的学习率。

常用的初始设置:

  • beta1=0.9
  • beta2=0.999
  • Learning_rate = 1e-3 or 5e-4
1
2
3
4
5
6
7
8
9
first_moment = 0
second_moment = 0
for t in range(1,num_iterations):
dx = compute_gradient(x)
first_moment = beta1 * first_moment + (1-beta1)*dx
second_moment = beta2 * second_moment + (1-beta2)*dx*dx
first_unbias = first_moment / (1-beta1**t)
second_unbias = second_moment /(1-beta2**t)
x -= leaning_rate * first_unbias / (np.sqrt(second_unbias)+1e-7)

adam的优点很明显——既不会越过很多(Momentum的缺点),又可以在各个方向平滑更新(Adagrad优点)

一般做实验的时候,常用adam

学习率的设置

前面是通过梯度来更新学习率,但初始学习率是一个超参,我们需要来了解学习率如何设置。因为学习率过大,会很容易越过损失函数的最优点;学习率过小,则会导致损失函数更新很慢。如下图所示

常用的设置是:使用逐渐衰减(decay) 的学习率, 即越来越小。这是因为越接近optimal,越容易越过这个最优解,因此我们要让参数更新没那么快,以找到最优点。

  • step decay: 每训练N个 iteration/epoch ,就除以常数
  • exponention decay: $\alpha = \alpha_0e^{-kt}$
  • 1/t decay :$\alpha = \frac{\alpha_0}{1+kt}$

Step

这就是我们使用了学习率下降后的曲线图,比如在SGD+Momentum这个优化方法中,学习率并不会自适应更新,因此我们学一段时间后就让学习率变小一点,这样能使loss持续不断地减小。比如说一开始把学习率设置为0.1,然后每30代乘以一个0.1

Cosine

使用Step LR Schedule会带来的一大问题是:我们不知道训练几代去下调一次学习率,也不知道下调到多少达到的效果最好,因此需要多次试错。因此我们可以用一个平滑的曲线代替阶段式的调优:这里选取的就是余弦函数

这样,学习率也从阶段式下降变成了一条平滑下降的曲线,如下图所示:

Loss 曲线如下所示:

相比于 Step,采用Cosine策略,我们要调整的超参就少了很多——只有两个必要的,而不需要引入新的超参

一个是初始的Learning Rate,还有一个是大写的T,也就是我们要训练的epoch数量

Linear

当然,我们还可以采用线性下降的策略.

我们要有一个观念,并不存在那种学习率策略是更好的,这取决与训练的模型。因此Cosine和Linear是无高下之分的。

Inverse sqrt

还有一种策略是让 出示学习率除以迭代次数的平方根,公式如下:

这种学习率变化策略是:只在高学习率停留很小一段时间。在低学习率停留较长时间。(和Cosine策略有点相反)

正则化

我们知道过拟合的坏处是很大的,因此我们要阻止过拟合的发生。这时,就需要正则化

Add term to the loss

我们在之前反向传播的时候就提醒过,在Loss function中需要 添加正则化项:

其中,$\lambda$是超参数,代表正则化的强度

常使用的$R(W)$如下:

  • L2 regularization $R(W) = \sumk\sum_lW{k,l}^2$(Weight decay)
  • L1 regularization $R(W) = \sumk\sum_l|W{k,l}|$
  • Elastic net(L1+L2) $R(W) = \sumk\sum_l\beta W{k,l}^2+|W_{k,l}|$

Drop out 减小特征的捕获

我们可以随机丢失一些output,来让模型简化,减小冗余,我们不希望很多神经元去学习同一个特征,我们希望神经网络中的神经元有自己的“特长”,各自负责各自的特征。这样就可以减少过拟合发生的概率:

一般来说,随机drop的概率设为0.5。这就给神经网络的训练增加了很大的随机性

1
2
3
4
5
6
7
H1 = np.maximum (0,np.dot(W1,X)+b1)
U1 = np.random.rand(*H1.shape)<p # first dropout mask
H1 *= U1 # drop
H2 = np.maximum(0,np.dot(W2,H1)+b2)
U2 = np.random.read(*H2.shape)<p
H2 *= U2 # drop
out = np.dot(W3,H2)+b3

此外,drop out还可以减小计算压力,如果每一层都要做全连接的话,这种计算量是无法想象的

在上图中,我们可以看到两个经典的神经网络是在哪几层进行Drop out的。在之后的一些神经网络架构中,如GoogleNet,ResNet,它们使用全局平均池化层来代替全连接层,这样就不需要dropout调节了

Drop out Training and Testing

在训练集和测试集上,Drop out 层的功能是不一样的。

在训练时,Drop out给神经网络增加可随机性:

  • y代表 Output
  • x代表 Input
  • z代表 Random Mask

这样,设p为每个神经元输出的概率,那么在训练集上,神经元有1-p的概率是不输出任何值的,但是在测试集中,Drop out再产生随机性就说不通了,要不然,神经网络今天把图片认成猫明天认成狗,就不好了。
因此,在测试集上,我们仍然保留每个神经经元的输出,但在每个输出的值上乘以概率p。这样一来,测试集和训练集上,每个神经元输出的值的期望是保持相同的: $(1-p)\times 0\times w+pw=wp$

因此,在测试集上,Drop out层的代码应该这么写:

1
2
3
H1 = np.maximum(0,np.dot(W1,X)+b1)*p
H2 = np.maximum(0,np.dot(W2,H1)+b2)*p
out = np.dot(W3,H2)+b3

Inverted dropout

普通dropout和Inverted dropout的思想是一样的,区别就在于什么时候做 rescaling. 前者是在测试推理时进行rescaling,而后者则是在推理时保持不变,在训练时做rescaling.

具体实现就是:在训练时让一部分神经元失活,让 另一部分神经元的输出值除以p,即变大。

但不管怎么样,两种dropout的输出值的数学期望是相等的,设计Inverted dropout的出发点是不想去动测试集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p =0.5
def train_step(X):
H1 = np.maximum (0,np.dot(W1,X)+b1)
U1 = np.random.rand(*H1.shape)<p /p #我们把 /p 写在这里
H1 *= U1 # drop
H2 = np.maximum(0,np.dot(W2,H1)+b2)
U2 = np.random.read(*H2.shape)<p /p
H2 *= U2 # drop
out = np.dot(W3,H2)+b3

def predict(X):
H1 = np.maximum(0,np.dot(W1,X)+b1)
H2 = np.maximum(0,np.dot(W2,H1)+b2)
out = np.dot(W3,H2)+b3

正则化一般模板

正则化在训练时和推理(测试)时的实现是不同的,我们可以用这样的公式去表示:

  • Training : 增加一些随机性
  • Testing:将这些随机性公摊到输出值上(有时候是近似的)

像刚才,我们介绍的Dropout就是一个很好的例子。此外,比如Batch Normalization也是类似的,它在训练时选取随机的minibatch去做归一化;但是在测试的时候,就是用规定下来的数据$\mu,\sigma^2$ 去给输入值做归一化

Data augumentation 增加训练数据

增加训练数据的好处主要有两个:

  1. 在一些神经网络中,输入值的数据量可能没有很大。比如说要用神经网络来训练医学影像,但是患者是有限的,拍片数量也是有限的,因此我们需要想办法增加训练集的数据。
  2. 如果训练集过小,神经网络的训练效果是不如机器学习的,因为很容易发生过拟合的情况,由此,我们更需要增加训练数据,避免过拟合。从这个角度看 ,增加训练数据的目的和正则化的目的是一样的,所以放在一起讲

下面来讲几种增加训练数据的方法

Transform image

反转图片,如下

和反转图片相似的,我们还可以扭曲部分图片像素,来达到增加训练数据的效果

Color jitter

我们可以随机调整图片的对比度、亮度,如下

Random Crops and Scales

我们可以对一张照片进行裁剪和缩放,用相等的框框从图中选取$l\times l$的图片

在推理的时候,我们可以平均多个crops的预测结果,已达到正则化的目的

随机池化大小

随机池化大小可以简单理解为对图片实行不同程度的模糊。其实现原理就是将不同尺度的像素块(patch)合在一起

Cutout

在小数据集上还常用Cutout方法,就是用一个方块挡住图片中不同的位置

Mixup

还有一种方法就是Mixup,将两张图片按照不同的权重合并起来,以增加训练数据

早停法

还有一个技巧,是在合适的时候停止训练神经网络,当我们看到验证集的正确率开始下降、或者已经训练很长时间的时候,我们就该停止训练。这被称为(早停法),如上图所示

在实际训练中

  • 在大型的全连接网络中,我们需要使用Drop out
  • 训练的时候使用Batch Normalization和各种常用的data augmentation
  • 在小数据集上尝试随机池化,cutout,mixup等方法
  • 使用early stopping 和 ensembles model

超参的选择

在神经网络中非常重要的两个超参数就是Weight decay以及Learning rate,为了找到合适的超参,我们肯定需要大量循环。一个方法就是将两者的取值写成列表,然后穷举搭配。比如:

1
2
3
4
5
6
weight_decay_list = [1*10^(-4),1*10^(-3),1*10^(-2),1*10^(-1)]
learning_rate = [1*10^(-4),1*10^(-3),1*10^(-2),1*10^(-1)]

for weight in weight_decay_list:
for learning_rate in learing_rate_list:
#...

相比于确定多个值去做穷举,我们可以划定一个范围,然后在里面随机挑选超参搭配。运行多次后找到合适的超参方案:

比如说采用 loguniform函数,即连续对数均匀分布(在浮点数空间中的对数尺度上均匀分布)。然后每次训练的时候在这个函数中随机取值

Weight decay: log-uniform on $[1\times10^{-4},1\times10^{-1}]$;

Weight decay: log-uniform on $[1\times10^{-4},1\times10^{-1}]$;

在上面这个例子中,即在$[-4,-1]$中随机取值t,取$1\times 10^{-t}$作为参数

两种搜索方法辨析

在一个神经网络里面可能有很多超参数,有些超参数对神经网络的影响非常大,而有些超参对神经网络的影响几乎可以忽略不计。

此时,如果采用 Grid Search,如左下图所示,由于采用相等的间隔采样,我们很可能会略过最优值,而如果缩小采样间隔,会导致训练时间更长。

但是对于 Random Search,如右下图所示,更可能选择到好的重要参数,然后只要继续做微调即可

一般模式

最后我们来讲如何选择超参数

综上,我们可以归结为如下步骤:

  1. 验证loss计算的正确性

    • 是否加上正则项
  2. 确保能够overfit一个小样本集

    • 我们可以从cifar10中选取10~50张图片作为模型
    • 在不用正则项的情况下,初步调整学习率和初始化方法。
      • 如果loss始终不下降,说明学习率太小
      • 如果loss突然暴增,说明学习率太大,已经跳过了最优点
  3. 使用全部数据集寻找合适的学习率

    • 加上很小的weight decay之后,在100个iteration以内搜索能使loss快速下降的学习率
    • 一般可以尝试 0.1,0.01,0.001,0.0001
  4. 粗粒度搜索学习率和weight decay,观察验证集结果

    • 在第3步确定的学习率范围内随机搜索,并随机搜索 weight decay
    • 每个学习率和weight decay组合训练 ,一般取5个epoch
  5. 细粒度搜索学习率和weight decay,观察验证集结果

    • 缩小第四步的搜索范围,训练更长时间 (大概20个epoch)
  6. 根据learning curve 采取相应措施

    • 如果出现上面这种情况,说明初始化有问题,我们应该调整参数初始化

    • 若出现上面这种情况,loss一直降不下来,我们可以尝试衰减学习率。(采用不同的策略)

    • 如果衰减学习率之后,loss出现一个断层,之后平滑、不再下降。说明我们衰减的太早了,还需要多训练几个epoch

    • 如果随着时间的推移,发现训练集的正确率持续上升而测试集正确率却逐渐走低。说明出现了过拟合的情况,此时需要增加正则,并使用更多数据

    • 如果训练集合测试集的准确率考的太近,说明我们选用的模型太小了,需要考虑使用更大的模型

  7. 如果还是不理想,那么需要回到第五步再寻找

总结

-------------本文结束,感谢您的阅读-------------