Shark源码分析(七):神经网络
对于神经网络这里应该就不用叙述了吧,之后可能会写一些关于深度学习方面的博客。这里要介绍的神经网络,名字应该叫做前馈神经网络(Feed-Forward Networks)。至于这个神经网络为什么要叫前馈,是因为这个网络在拓扑结构上不含有反向边,也就是没有环。而不是指的信号不能反向地传递。
对于单个神经元,Shark提供了几种激活函数以供选择:
- Logistic函数,输出范围是[0, 1]。其实在这种类型的网络中使用该函数不是一个很好的选择,因为在该函数的大部分区域上,其导数值都非常接近于0。在基于梯度的方法进行训练中,导致参数无法进行有效地更新。这也就是常说的梯度弥散问题。这需要在目标函数上做出一些工作。
- 双曲正切函数(tanh),输出范围是[-1, 1]。这个函数与Logistic函数有着同样的问题。
- Fast Sigmoid函数,是一种类sigmoid函数,较之前两个函数的计算速度更快,是一种长尾类型的函数,意味着梯度值不会消散的特别快。其具体的形式为:
f(x)=x1+|x| - 修正线性单元(Rectified Linear Units),是最近使用非常广泛的激活函数。其形式如下:
f(x)={ 0,x<0x,x≥0
当x大于0时,其导数值是一直存在的,且恒为1。这个函数构建的网络优化起来也特别方便。
5.线性函数,将它作为隐层神经元的激活函数可能不太合适。因为线性函数的组合结果还是线性函数,导致整个网络没有办法拟合特别复杂的函数。在中间的一些层次上也可以使用该函数,如果将后一层神经元的个数定的较少的话,可以达到一个降维的作用。
NeuronBase类
作为所有类型神经元的基类,它定义了神经元的激活函数以及激活函数对应的导数。该类定义在<include/Shark/Models/Neuron.h>
。
template<class Derived> //注意到模板的参数是一个派生类类型,这个玄机我们之后会说
class NeuronBase{
private:
template<class T>
struct Function{ //定义神经元的激活函数
typedef T argument_type;
typedef argument_type result_type;
static const bool zero_identity = false;
Function(NeuronBase<Derived> const* self):m_self(static_cast<Derived const*>(self)){} //这一段代码也是需要注意的一点
//重载括号运算符,计算激活函数的输出
result_type operator()(argument_type x)const{
return m_self->function(x);
}
Derived const* m_self;
};
//定义激活函数的导数,与Function结构是类似的
template<class T>
struct FunctionDerivative{
typedef T argument_type;
typedef argument_type result_type;
static const bool zero_identity = false;
FunctionDerivative(NeuronBase<Derived> const* self):m_self(static_cast<Derived const*>(self)){}
result_type operator()(argument_type x)const{
return m_self->functionDerivative(x);
}
Derived const* m_self;
};
public:
//对于输入的每一项,计算其对应的激活值
template<class E>
blas::vector_unary<E, Function<typename E::value_type> > operator()(blas::vector_expression<E> const& x)const{
typedef Function<typename E::value_type> functor_type;
return blas::vector_unary<E, functor_type >(x,functor_type(this));
}
template<class E>
blas::matrix_unary<E, Function<typename E::value_type> > operator()(blas::matrix_expression<E> const& x)const{
typedef Function<typename E::value_type> functor_type;
return blas::matrix_unary<E, functor_type >(x,functor_type(this));
}
//计算输入对应的激活函数的导数值
template<class E>
blas::vector_unary<E, FunctionDerivative<typename E::value_type> > derivative(blas::vector_expression<E> const& x)const{
typedef FunctionDerivative<typename E::value_type> functor_type;
return blas::vector_unary<E, functor_type >(x,functor_type(this));
}
template<class E>
blas::matrix_unary<E, FunctionDerivative<typename E::value_type> > derivative(blas::matrix_expression<E> const& x)const{
typedef FunctionDerivative<typename E::value_type> functor_type;
return blas::matrix_unary<E, functor_type >(x,functor_type(this));
}
};
需要在子类中定义具体的激活函数以及其对应的导数形式,接下来我们来看一个具体的神经元类。
struct LogisticNeuron : public detail::NeuronBase<LogisticNeuron>{
template<class T>
T function(T x)const{
return sigmoid(x);
}
template<class T>
T functionDerivative(T y)const{
return y * (1 - y);
}
};
其实这个类的实现还是非常简单的。但是将其与基类联系起来看,其实是利用模板来实现多态。这种方法我也是第一次碰到。我们还是利用一点点的篇幅来介绍下这种技术。
我们把传统的多态实现方式称为动态多态,而模板的实现则是静态多态。区别如下:
- 动态多态的多态性是在运行期决定的,而静态多态则是在编译期决定的。
- 动态多态的实现需要更多空间上的开销,每个对象会因为一个虚函数而增加4 bytes,而静态多态则没有这个问题。
- 动态多态的实现需要更多时间的开销,虚函数的调用在时间上会比普通函数多一次整形加法和指针的间接引用。
- 动态多态是编译器内置的实现方式,而静态多态则会额外带来使用的复杂性。
- 动态多态中虚函数不能通过内联来优化执行效率。
其余神经元与LogisticNeuron类的形式差不多,这里就不再具体介绍了。但是在其中会发现一个原来没有介绍过的神经元类型。
template<class Neuron>
struct DropoutNeuron: public detail::NeuronBase<DropoutNeuron<Neuron> >{
DropoutNeuron():m_probability(0.5),m_stochastic(true){}
template<class T>
T function(T x)const{
if(m_stochastic && Rng::coinToss(m_probability)){
return T(0);
}
else if(!m_stochastic){
return (1-m_probability)*m_neuron.function(x);
}else{
return m_neuron.function(x);
}
}
template<class T>
T functionDerivative(T y)const{
if(!m_stochastic){
return (1-m_probability)*m_neuron.functionDerivative(y/ (1-m_probability));
}else{
return m_neuron.functionDerivative(y);
}
}
void setProbability(double probability){m_probability = probability;}
void setStochastic(bool stochastic){m_stochastic = stochastic;}
private:
double m_probability; //将输出甚至为0的概率
bool m_stochastic;
Neuron m_neuron;
};
这个类是对我们之前介绍的基本神经元类型,如LogisticNeuron类,的一种封装。并在其中应用了dropout技术。该技术最近也是比较火。它的思想非常的简单,通过将神经元的输出以一定的概率设置为0,来达到减小模型过拟合的概率。这里有一个问题就是,当m_stochastic这个变量被设置为true时是dropout,但如果被设置为false呢?
FFNet类
该类是定义网络具体结构的类。神经网络中所有的隐层单元的激活函数都是一样的。但是输出层的激活函数与隐层的可以不一致。该类定义在<include/Shark/Models/FFNet.h>
中。
首先来介绍下网络的几种连接方式。
struct FFNetStructures{
enum ConnectionType{
Normal, //没有跨层之间的连接
InputOutputShortcut, //有输入层到输出层的连接
Full //网络中的某一层与其下所有层都是有连接的
};
};
template<class HiddenNeuron,class OutputNeuron>
class FFNet :public AbstractModel<RealVector,RealVector>
{
//网络中的神经元数,输入层神经元数,输出层神经元数
std::size_t m_numberOfNeurons;
std::size_t m_inputNeurons;
std::size_t m_outputNeurons;
//层间神经元连接的权值矩阵
std::vector<RealMatrix> m_layerMatrix;
//输入层与输出层间的连接权值,当且仅当连接方式选择了InputOutputShortcut,这个矩阵才是有意义的
RealMatrix m_inputOutputShortcut;
//这个矩阵是从输出层方向看整个网络得到的权值连接矩阵,相当于是m_layerMatrix的转置,主要用于反传过程
std::vector<RealMatrix> m_backpropMatrix;
//神经元的偏置,只有隐层单元和输出神经元可能有这一项
RealVector m_bias;
// 表示隐藏层、输出层神经元的类型
HiddenNeuron m_hiddenNeuron;
OutputNeuron m_outputNeuron;
struct InternalState: public State{
//存储输入是上一个batch数据时,网络中每一个神经元的激活值
RealMatrix responses;
void resize(std::size_t neurons, std::size_t patterns){
responses.resize(neurons,patterns);
}
};
public:
FFNet()
:m_numberOfNeurons(0),m_inputNeurons(0),m_outputNeurons(0){
m_features|=HAS_FIRST_PARAMETER_DERIVATIVE;
m_features|=HAS_FIRST_INPUT_DERIVATIVE;
}
std::string name() const
{ return "FFNet"; }
std::size_t inputSize()const{
return m_inputNeurons;
}