CNN 基础之 Dropout

Dropout 是一种非常非常通用的解决深层神经网络中 overfitting 问题的方法, 过程极其简单, 在调试算法中效果也非常有效, 几乎是我设计网络过程中必用的技巧.

概述

深层神经网络的参数非常非常多, 一般情况下设计的神经网络非常容易 overfitting, 在 CNN 中, Pooling 层具有一定的防止 overfitting 的作用, 但是, CNN 的结构一般是多个 conv factory 之后需要使用 full connect 来做分类等任务, 由于在 full connect layer 中参数密度比较高, 非常容易 overfitting, 而 Dropout 可以用来解决这里的 overfitting 问题[^1].

具体过程


如上图, 左边是我们常见的 full connect layer, 右边是使用了 dropout 之后的效果. 其操作方法是, 首先设定一个 dropout ratio \(\sigma\), \(\sigma\) 是超参数, 范围设置为 \(\left (0,1\right)\), 表示在 Forward 阶段需要随机断开的连接的比例. 每次 Forward 的时候都要随机的断开该比例的连接, 只更新剩下的 weight. 最后, 在 test/predict 的时候, 使用全部的连接, 不过, 这些 weights 全部都需要乘上 \(1-\sigma\). 不过, 在具体的实现中略有不同, 参下面的源码解释.

Side Effect

Dropout 除了具有防止 overfitting 的作用之外, 还有 model ensemble 的作用.
我们考虑, 假设 \(\sigma=0.5\), 如果 Forward 的次数足够多(例如无穷次), 每次都有一半的连接被咔嚓掉, 在整个训练过程中, 被咔嚓掉的连接的组合是 \(2^n\), 那么, 留下的连接的组合种类也是 \(2^n\), 所以, 这就相当于我们训练了 \(2^n\) 个模型, 然后 ensemble 起来.

MXNet 源码

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
template<typename xpu>
class DropoutOp : public Operator {
public:
explicit DropoutOp(DropoutParam param) {
// param.p 是 dropout ratio
this->pkeep_ = 1.0f - param.p;
}
virtual void Forward(const OpContext &ctx,
const std::vector<TBlob> &in_data,
const std::vector<OpReqType> &req,
const std::vector<TBlob> &out_data,
const std::vector<TBlob> &aux_states) {
using namespace mshadow;
using namespace mshadow::expr;
CHECK_EQ(in_data.size(), 1);
if (ctx.is_train) {
CHECK_EQ(out_data.size(), 2);
}
Stream<xpu> *s = ctx.get_stream<xpu>();
Tensor<xpu, 2> data = in_data[dropout::kData].FlatTo2D<xpu, real_t>(s);
Tensor<xpu, 2> out = out_data[dropout::kOut].FlatTo2D<xpu, real_t>(s);
if (ctx.is_train) {
Tensor<xpu, 2> mask = out_data[dropout::kMask].FlatTo2D<xpu, real_t>(s);
Random<xpu> *prnd = ctx.requested[dropout::kRandom].get_random<xpu, real_t>(s);
// 均匀采样, 采样概率是留下的概率, 根据这个概率来咔嚓连接, 但是, 把留下的值全部除以pkeep,
// 这样, 在 test/predict 的时候就不需要像上面描述的那样乘以 1-sigma 了
mask = F<mshadow_op::threshold>(prnd->uniform(mask.shape_), pkeep_) * (1.0f / pkeep_);
Assign(out, req[dropout::kOut], data * mask);
} else {
Assign(out, req[dropout::kOut], F<mshadow_op::identity>(data));
}
}

CAFFE 源码

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
void DropoutLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
NeuronLayer<Dtype>::LayerSetUp(bottom, top);
threshold_ = this->layer_param_.dropout_param().dropout_ratio();
DCHECK(threshold_ > 0.);
DCHECK(threshold_ < 1.);
scale_ = 1. / (1. - threshold_);
uint_thres_ = static_cast<unsigned int>(UINT_MAX * threshold_);
}
template <typename Dtype>
void DropoutLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
const Dtype* bottom_data = bottom[0]->cpu_data();
Dtype* top_data = top[0]->mutable_cpu_data();
unsigned int* mask = rand_vec_.mutable_cpu_data();
const int count = bottom[0]->count();
if (this->phase_ == TRAIN) {
// Create random numbers
// 需要保留的数据
caffe_rng_bernoulli(count, 1. - threshold_, mask);
for (int i = 0; i < count; ++i) {
// 同理MXNet中的解释
top_data[i] = bottom_data[i] * mask[i] * scale_;
}
} else {
caffe_copy(bottom[0]->count(), bottom_data, top_data);
}
}

使用方法

MXNet

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
43
import mxnet as mx
def get_symbol(num_classes = 1000):
input_data = mx.symbol.Variable(name="data")
# stage 1
conv1 = mx.symbol.Convolution(
data=input_data, kernel=(11, 11), stride=(4, 4), num_filter=96)
relu1 = mx.symbol.Activation(data=conv1, act_type="relu")
pool1 = mx.symbol.Pooling(
data=relu1, pool_type="max", kernel=(3, 3), stride=(2,2))
lrn1 = mx.symbol.LRN(data=pool1, alpha=0.0001, beta=0.75, knorm=1, nsize=5)
# stage 2
conv2 = mx.symbol.Convolution(
data=lrn1, kernel=(5, 5), pad=(2, 2), num_filter=256)
relu2 = mx.symbol.Activation(data=conv2, act_type="relu")
pool2 = mx.symbol.Pooling(data=relu2, kernel=(3, 3), stride=(2, 2), pool_type="max")
lrn2 = mx.symbol.LRN(data=pool2, alpha=0.0001, beta=0.75, knorm=1, nsize=5)
# stage 3
conv3 = mx.symbol.Convolution(
data=lrn2, kernel=(3, 3), pad=(1, 1), num_filter=384)
relu3 = mx.symbol.Activation(data=conv3, act_type="relu")
conv4 = mx.symbol.Convolution(
data=relu3, kernel=(3, 3), pad=(1, 1), num_filter=384)
relu4 = mx.symbol.Activation(data=conv4, act_type="relu")
conv5 = mx.symbol.Convolution(
data=relu4, kernel=(3, 3), pad=(1, 1), num_filter=256)
relu5 = mx.symbol.Activation(data=conv5, act_type="relu")
pool3 = mx.symbol.Pooling(data=relu5, kernel=(3, 3), stride=(2, 2), pool_type="max")
# stage 4
flatten = mx.symbol.Flatten(data=pool3)
fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=4096)
relu6 = mx.symbol.Activation(data=fc1, act_type="relu")
# dropout 的使用, 在 full connect 后面
dropout1 = mx.symbol.Dropout(data=relu6, p=0.5)
# stage 5
fc2 = mx.symbol.FullyConnected(data=dropout1, num_hidden=4096)
relu7 = mx.symbol.Activation(data=fc2, act_type="relu")
# dropout, 直接接在 full connect 后面
dropout2 = mx.symbol.Dropout(data=relu7, p=0.5)
# stage 6
fc3 = mx.symbol.FullyConnected(data=dropout2, num_hidden=num_classes)
softmax = mx.symbol.SoftmaxOutput(data=fc3, name='softmax')
return softmax

CAFFE

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
layer {
name: "fc6"
type: "InnerProduct"
bottom: "pool5"
top: "fc6"
param {
lr_mult: 1
decay_mult: 1
}
param {
lr_mult: 2
decay_mult: 0
}
inner_product_param {
num_output: 4096
weight_filler {
type: "gaussian"
std: 0.005
}
bias_filler {
type: "constant"
value: 0.1
}
}
}
layer {
name: "relu6"
type: "ReLU"
bottom: "fc6"
top: "fc6"
}
# 方法类似, 接在 full connect 后面, 指定 dropout ratio
layer {
name: "drop6"
type: "Dropout"
bottom: "fc6"
top: "fc6"
dropout_param {
dropout_ratio: 0.5
}
}
layer {
name: "fc7"
type: "InnerProduct"
bottom: "fc6"
top: "fc7"
param {
lr_mult: 1
decay_mult: 1
}
param {
lr_mult: 2
decay_mult: 0
}
inner_product_param {
num_output: 4096
weight_filler {
type: "gaussian"
std: 0.005
}
bias_filler {
type: "constant"
value: 0.1
}
}
}
layer {
name: "relu7"
type: "ReLU"
bottom: "fc7"
top: "fc7"
}
layer {
name: "drop7"
type: "Dropout"
bottom: "fc7"
top: "fc7"
dropout_param {
dropout_ratio: 0.5
}
}

[^1]: 最近也有一些网络不使用 Dropout, 例如: Network in Network 使用的是 global average pooling 来 handle 的 overfitting 问题, 以及 Kaiming 的 Residual Network 也没有使用 dropout, Residual Net 甚至没有使用 Pooling, 而是用 convolution 操作来搞定的降维问题.