[置顶] 深度学习(DL)与卷积神经网络(CNN)学习笔记随笔-04-基于Python的LeNet之MLP

原文地址可以查看更多信息

  本文主要参考于:Multilayer Perceptron
  python源代码(github下载  CSDN免费下载)

  本文主要介绍含有单隐层的MLP的建模及实现。建议在阅读本博文之前,先看一下LR的实现。因为LR是简化版的MLP。LR不含有单隐层,则其输入层直接连接到输出层。从何处可以看出LR是输入层直接连接输出层?借用上一博文的公式: P(Y=i|x,W,b)=softmaxi(Wx+b) 。其中, x 是输入层, softmax 是激活函数, P 就是输出层了。我们将其化简并转换为一般神经网络表达式: f(x)=g(Wx+b) ,其中 g 就是激活函数, f(x) 就是输出层了。可见输入层经过激活函数得到的结果就是输出了。很简单吧。

  那么MLP的模型公式和LR又有什么不同呢?下面来看一下MLP的模型建立。

一、模型


  从MLP的结构图中可以看出输入层与隐藏层全连接,然后,隐藏层与输出层全连接。那么整体的函数映射就是 fRDRL ,其中 D 是输入向量 x 的维度, L 是输出向量 f(x) 的维度。用矩阵表示整个三层之间的关系如下:

   f(x)=G(W(2)(S(W(1)x+b(1)))+b(2))

  其中, b(1),b(2) 分别是三层之间(输入层与隐层、隐层与输出层之间)的偏置向量; W(1),W(2) 分别是三层之间的权值矩阵;而 S,G 是分别是三层之间的激活函数。

  对于连接单隐层的的表达式 h(x)=Φ(x)=S(W(1)x+b(1)) ,其激活函数 S 常用的有 tanh(a)=(eaea)(ea+ea) sigmoid(a)=1(1+ea) 函数。在这里,我们将使用 tanh 作为激活函数,因为它通常能更快的达到训练目标。

  对于连接输出层的表达式 o(x)=G(W(2)h(x)+b(2)) ,我们应该不是很陌生,就是在篇头或上一篇博文中讲到的LR的模型表达式,在那里面其激活函数用的是 softmax ,它可以用来多分类,那么在这里, G 也是采用 softmax 多分类器来分类。

  接下来就是训练模型获取最佳参数,我们这里仍然采用MSGD(批量随机梯度下降法)方法。这里我们需要学习的参数为 θ={W(2),b(2),W(1),b(1)} 。同样,对于用链式法则求梯度 /θ ,我们就用theano已经实现的T.grad()方法。

  讲到这里小伙伴们或许有疑问了:没有代价函数怎么求梯度啊?也就是 /θ 中的 是什么啊?很简单,就是和LR中的代价函数基本是一样的(在后边还会讲到cost代价函数为什么是基本一样而不是一样),因为结构都是一样的,我们同样要处理的是对于手写数字MNIST的识别。最小化方法也是采用负对数似然函数,所以这里的 一清二楚了吧,还不清楚的,可以翻看LR的(二、定义代价函数)。

二、MLP代码实现

  我们主要关注实现含单隐层的MLP(只要单隐层的实现了,多隐层的就是多实例化几个隐层而已)。MLP的实现包括3部分:输入层、隐层、输出层。

  第一层:输入层就是我们直接的输入数据 x ,将其直接输入到第二层;
  第二层:隐层的功能是用 tanh 函数处理第一层的输入数据,并将结果作为第三层的输入数据。因此需要我们用代码实现,即根据LR实现一个隐层类;
  第三层:输出层的功能是将第二层的输入数据经过 softmax 函数,然后进行分类输出。因此和LR是一样的,所以这里直接调用上一篇定义的LR类-LogisticRegression类。

  先提前讲一下隐层权值的初始化问题。在LR类中,权值矩阵初始化为0。然而在隐层中就不能再初始化为0了,而是依据激活函数从symmetric interval(对称间隔)中均匀采样。这样初始化是为了确保在训练早期,每一个神经元都可以向后传播(upward)激活信息,向前传播(backward )梯度数据。也就是说在这一层用到了梯度反向传播,那么权值矩阵就不能初始化为0,不知道解释的如何?原文:(This initialization ensures that, early in training, each neuron operates in a regime of its activation function where information can easily be propagated both upward (activations flowing from inputs to outputs) and backward (gradients flowing from outputs to inputs))。

  那么具体的均匀采样范围是什么呢?
  对于函数 tanh 来说,interval为: [6fanin+fanout,6fanin+fanout] ,其中 fanin 是第 (i1) 层的单元个数; fanout 是第 i 层的单元个数。如果对应到本实验,第 (i1) 层就是输入层,输入层输入数据是样本数据,因此其单元个数是样本数据的长度;第 i 层就是隐层,其单元个数是需要实例化时传入的。具体的看下方代码。

  对于函数 sigmoid 来说,interval为: [46fanin+fanout,46fanin+fanout]
  
  可以从文献Xavier10中获取以上范围的来因。是一个大牛写的论文,如果能看懂也很牛。
部分代码:

 class HiddenLayer(object):
    def __init__(self, rng, input, n_in, n_out, W=None, b=None, activation=T.tanh):
        ''' 初始化函数!HiddenLayer实例化时调用该函数。该层与输入层是全连接,激活函数为tanh。 参数介绍: rng 类型为:numpy.random.RandomState。 rng 功能为:rng是用来产生随机数的实例化对象。本类中用于对W进行随机数初始化。而非0值初始化。 input 类型为:符号变量T.dmatrix input 功能为:代表输入数据(在这里其实就是传入的图片数据x,其shape为[n_examples, n_in],n_examples是样本的数量) n_in 类型为:int n_in 功能为:每一个输入样本数据的长度。和LR中一样,比如一张图片是28*28=784, 那么这里n_in=784,意思就是把图片数据转化为1维。 n_out 类型为:int n_out 功能为:隐层单元的个数(隐层单元的个数决定了最终结果向量的长度) activation 类型为:theano.Op 或者 function activation 功能为:隐层的非线性激活函数 '''
        self.input = input

        # 根据博文中的介绍,W应该按照均匀分布来随机初始化,其样本数据范围为:
        # [sqrt(-6./(fin+fout)),sqrt(6./(fin+fout))]
        # 根据博文中的说明,fin很显然就是n_in了,因为n_in就是样本数据的长度,即输入层的单元个数。
        # 同样,fout就是n_out,因为n_out是隐层单元的个数。
        # rng.uniform()的意思就是产生一个大小为size的矩阵,
        # 矩阵的每个元素值最小是low,最大是high,且所有元素值是随机均匀采样。
        if W is None:
            W_values = numpy.asarray(
                rng.uniform(
                    low=-numpy.sqrt(6. / (n_in + n_out)),
                    high=numpy.sqrt(6. / (n_in + n_out)),
                    size=(n_in, n_out)
                ),
                dtype=theano.config.floatX
            )
            # 如果激活函数是sigmoid的话,每个元素的值是tanh的4倍。
            if activation == theano.tensor.nnet.sigmoid:
                W_values *= 4
            W = theano.shared(value=W_values, name='W', borrow=True)

        # 偏置b初始化为0,因为梯度反向传播对b无效
        if b is None:
            b_values = numpy.zeros((n_out,), dtype=theano.config.floatX)
            b = theano.shared(value=b_values, name='b', borrow=True)       

        self.W = W
        self.b = b

        # 计算线性输出,即无激活函数的结果,就等于最基本的公式 f(x)=Wx+b
        # 如果我们传入了自己的激活函数,那么就把该线性输出送入我们自己的激活函数,
        # 此处激活函数为非线性函数tanh,因此产生的结果是非线性的。
        lin_output = T.dot(input, self.W) + self.b
        self.output = (
            # 这个表达式其实很简单,就是其他高级语言里边的三目运算
            # condition?"True":"false" 如果条件(activation is None)成立,
            # 则self.output=lin_ouput
            # 否则,self.output=activation(lin_output)
            lin_output if activation is None
            else activation(lin_output)
        )
        self.params = [self.W, self.b]

  根据HiddenLayer类计算完结果后,我们相当于计算了公式 h(x)=Φ(x)=S(W(1)x+b(1)) ,下面我们需要计算公式 o(x)=G(W(2)h(x)+b(2)) ,然后根据前文的分析,第二个公式就是我们之前实现的LR。因此,我们只需要将隐层的结果作为LR的输入,即可实现MLP的功能。下面,我们来写一下MLP的代码:

class MLP(object):
    ''' 多层感知机是一个前馈人工神经网络模型。它包含一个或多个隐层单元以及非线性激活函数。 中间层通常使用tanh或sigmoid作为激活函数,顶层(输出层)通常使用softmax作为分类器。 '''
    def __init__(self, rng, input, n_in, n_hidden, n_out):

        ''' rng, input在前边已经介绍过。 n_in : int类型,输入数据的数目,此处对应的是输入的样本数据。 n_hidden : int类型,隐层单元数目 n_out : int类型,输出层单元数目,此处对应的是输入样本的标签数据的数目。 '''
        # 首先定义一个隐层,用来连接输入层和隐层。
        self.hiddenLayer = HiddenLayer(
            rng=rng,
            input=input,
            n_in=n_in,
            n_out=n_hidden,
            activation=T.tanh
        )
        # 然后定义一个LR层,用来连接隐层和输出层
        self.logRegressionLayer = LR(
            input=self.hiddenLayer.output,
            n_in=n_hidden,
            n_out=n_out
        )
        # 规则化,常用的是L1和L2。是为了防止过拟合。
        # 其计算方式很简单。具体规则化的内容在文章下方详细说一下
        # L1项的计算公式是:将W的每个元素的绝对值累加求和。此处有2个W,因此两者相加。
        self.L1 = (
            abs(self.hiddenLayer.W).sum()
            + abs(self.logRegressionLayer.W).sum()
        )
        # L2项的计算公式是:将W的每个元素的平方累加求和。此处有2个W,因此两者相加。
        self.L2_sqr = (
            (self.hiddenLayer.W ** 2).sum()
            + (self.logRegressionLayer.W ** 2).sum()
        )

        # 和LR一样,计算负对数似然函数,计算误差。
        self.negative_log_likelihood = (
            self.logRegressionLayer.negative_log_likelihood
        )
        self.errors = self.logRegressionLayer.errors
        self.params = self.hiddenLayer.params + self.logRegressionLayer.params

        self.input = input

  现在就来解释一下篇头中提到的代价函数是基本一样的原因。
  首先来说一下规则化。当我们训练模型试图让它在面对新的输入样本时产生更好的结果(就是有更好的泛化能力),我们往往会采用梯度下降方法。而本实验中用到的MSGD方法却没有考虑到一个问题,那就是过拟合现象。一旦出现过拟合,那么在面对新的样本时很难有好的结果。为了控制过拟合,一个比较有效的方法就是规格化。就是给每个参数 θ (对应于 W 中的每一个元素)增加一个惩罚项,如果某个参数的系数非常大那么 θ 就会接近于0了。那么什么是L1和L2规格化呢?下面看一个公式,假设我们的代价函数是:

NLL(θ,D)=i=0|D|logP(Y=y(i)|x(i),θ)(R-1)

  那么规则化后的代价函数就是:
E(θ,D)=NLL(θ,D)+λR(θ)(R-2)

  就是加入了一个 λR(θ) 。在我们的实验中,具体的公式如下:
E(θ,D)=NLL(θ,D)+λ||θ||pp(R-3)

  其中:
||θ||p=j=0|θ||θj|p1p(R-4)

  不难发现,当把公式 (R4) 带入到公式 (R3) 后, 1p 次方与 p 次方约掉,就剩下了:
E(θ,D)=NLL(θ,D)+λj=0|θ||θj|p(R-5)

   (R5) 就是 θ Lp 项; λ 是一个很重的规则化参数。我们通常令 Lp 中的 p 取值为 1 或者 2 ,这就是命名为L1和L2的原因。

  原则上,当代价函数中增添了规则化项之后会使得网络拟合函数时更加平滑。所以,理论上,最小化 NLL R(θ) 的和等价于在拟合训练数据和找到一般性的解(分类超平面)之间找到最佳的权衡。根据Occam’s razor规则,最小化加入规则化项的公式会使得我们更容易找到拟合训练数据的解(分类超平面)。

  实际上一个模型有一个简单的超平面解并不意味着它的泛化能力很好。实验证明,含有这种规格化项的神经网络的泛化能力更好,尤其是在比较小的数据集上。
下面来看一下带有规则化项的代价函数代码如何写:

# 代价函数,这是一个符号变量,cost并不是一个具体的数值。当传入具体的数据后,
# 其才会有具体的数据产生。在原代价函数的基础上加入规则参数*规则项。
cost = (
        classifier.negative_log_likelihood(y) 
        + L1_reg * classifier.L1 
        + L2_reg * classifier.L2_sqr
)

  最后就是参数求梯度以及参数更新,基本和LR的区别不大。训练模型函数测试模型函数验证模型函数和LR相同。至此,代码实现基本告一段落,可以下载全部代码运行测试。其他的代码注释不多,因为应该比较简单。只有一个mlp.py文件。下载来,直接python mlp.py即可运行。如果有不懂的地方可以留言,也可以翻看上一篇LR的实现。本文所介绍的mlp去对mnist分类官方给出的一个结果是:

Optimization complete. Best validation score of 1.690000 % obtained at iteration 2070000, with test performance 1.650000 %
The code for file mlp.py ran for 97.34m

  我也正在跑代码,因为还是比较耗时的。这个网址里边记录了目前为止所做过的实验以及结果。有兴趣的可以看一下。最好的误差能达到0.23%了,天呢,接近100%的识别率了。

参考目录:
1. 官方教程
2. Stanford机器学习—第三讲. 逻辑回归和过拟合问题的解决 logistic Regression & Regularization
3. 深度学习(DL)与卷积神经网络(CNN)学习笔记随笔-03-基于Python的LeNet5之LR

你可能感兴趣的:(神经网络,深度学习,卷积神经网络,MLP,多层感知机)