2、模型的搭建及初始化
在我们读取数据集时我们已经知道输入的神经元个数,在构造NN类时我们需要设置分类数,也就是说我们已经知道输入和输出的神经元个数,所以只要知道隐藏层的层数及每层的神经元个数,那么整个神经网络也就搭建完成了。本程序中通过容器保存插入每层隐藏层神经元个数来确定隐藏层的层数和个数。搭建整个网络,其实就是依次创建每一层的权重和偏置的矩阵,并分别保存到存储权重和偏置的容器中。
对于模型得初始化,这里采用He Initialization初始化的方法,具体是什么鬼,接下来会进行详细得讲解。现在来看看我们是如何进行搭建和初始化网络的。
void NN::add_hidden_layer(const vector<size_t> &num_hiddens)
{
hiddens_.clear();
hiddens_.assign(num_hiddens.begin(), num_hiddens.end());
}
void NN::initial_layer(const size_t &input, const size_t &output)
{
RNG rnger(getTickCount());
//创建权重矩阵
Mat W(static_cast<int>(input), static_cast<int>(output), CV_32FC1);
//创建偏置矩阵
Mat b(1,static_cast<int>(output), CV_32FC1, 1.f);
//权重的初始化
rnger.fill(W, RNG::UNIFORM, cv::Scalar::all(-1.f / sqrt(input/2)), cv::Scalar::all(1.f / sqrt(input/2)));
W_.push_back(W);
b_.push_back(b);
}
void NN::initial_networks()
{
//初始化输入神经元个数
//如果未设置初始神经元个数
this->input_ = this->data_ptr->cols;
//初始化权重
RNG rnger(getTickCount());
if (this->hiddens_.size()){
size_t cur_row = this->input_;
auto it = this->hiddens_.begin();
while (it != this->hiddens_.end()){
initial_layer(cur_row, *it);
cur_row = *it;
++it;
}
initial_layer(cur_row, this->classes_);
}
else{
initial_layer(this->input_, this->classes_);
}
}
对于神经网络权重的初始化,有很多方式,通过控制权重参数初始值的范围,使得神经元的输入落在我们需要的范围内,以便梯度下降能够更快的进行。下面将介绍几种权重初始化方式以及各自的特点。
1、不要使用零初始化
在一般网络初始化中都不推荐进行零初始化。通过后面的反向传播过程(后面系列将提到),每层的权重与当前的输入有关,也就是说在前向传播的过程中没层的输入为零,最终得到的梯度也为零也就是梯度消失,这时计算得到权重不进行更新。当然,这只是我个人的理解,网上也有其他解释如下:
- 如果所有的参数都是0,那么所有神经元的输出都将是相同的,那在back propagation的时候同一层内所有神经元的行为也是相同的 --- gradient相同,weight update也相同。这显然是一个不可接受的结果。
- 这种初始化方法没有打破神经元之间的对称性,将导致收敛速度很慢甚至训练失败。
参考链接:
谷歌工程师:聊一聊深度学习的weight initialization
2、均匀随机初始化 or 正态随机初始化 or 随机初始化
目前来说这三种方法仍有大量的运用,但使用这几种方法要非常的小心,因为很容易得到造成局部最优,而使网络优化陷入困境。
- 如果你选择均匀随机初始化 or 正态随机初始化会有什么结果呢?
随着层数的增加,我们看到输出值迅速向0靠拢,在后几层中,几乎所有的输出值 x 都很接近0!回忆优化神经网络的back propagation算法,根据链式法则,gradient等于当前函数的gradient乘以后一层的gradient,这意味着输出值 x 是计算gradient中的乘法因子,直接导致gradient很小,使得参数难以被更新!
让我们将初始值调大一些,均值仍然为0,标准差现在变为1,下图是每一层输出值分布的直方图:
几乎所有的值集中在-1或1附近,神经元saturated了!注意到tanh在-1和1附近的gradient都接近0,这同样导致了gradient太小,参数难以被更新。
上述图和文字主要参考别人的,在这里列出,懒得自己重新想怎么写了。。。。
参考链接:
3、Xavier Initialization or He Initialization
这是目前来说比较推荐的方法,实际上He Initialization是Xavier Initialization的变种,我们可以从公式上可以看出:
Xavier Initialization:
He Initialization:
当然上述Xavier Initialization公式你在不同地方有不同的表示,比如:、这是基于不同的考虑,不如考虑输入和输出则选择带6的设置,但实际运用中只考虑输入,这对反向传播更为重要,所以选择文中列举的式子。
- 为什么要这样进行初始化呢?
Xavier初始化的基本思想是保持输入和输出的方差一致,这样就避免了所有输出值都趋向于0。其实,这样初始化也是有一定的理论依据的,在这里就不一一推导了,有兴趣的同学可以推一下试试,具体细节查看参考链接。
参考链接:
最佳实践:
权重初始化——对于深度网络,可以使用启发式来根据非线性激活函数初始化权重。在这里,并不是从标准正态分布绘图,而是用方差为k /n的正态分布初始化W,其中k的值取决于激活函数。尽管这些启发式方法不能完全解决梯度消失/爆炸问题,但它们在很大程度上有助于缓解这一问题。最常见的启发式方法是:
- 对于RELU(z)——将随机生成的W值乘以:
- 对于tanh(z) ——也被称为Xavier初始化。与前一个方法类似,但k的值设置为1而不是设置为2。
- 另一个常用的启发式方法:
梯度剪枝——这是处理梯度爆炸问题的另一种方法。我们可以设置一个阈值,如果一个梯度的选择函数大于这个设定的阈值,那么我们就将它设置为另一个值。例如,如果l2_norm(W)>阈值,则将L2范数超过特定阈值时的梯度值归一化为-W = W * threshold / l2_norm(W)。
偏置项初始化——因为每层偏置的梯度仅取决于该层的线性激活值,而不取决于较深层的梯度值。因此,对于偏置项不会存在梯度消失和梯度爆炸问题。如前所述,可以安全地将偏置b初始化为0。