代码改变世界

感知器与梯度下降

2014-03-07 14:40  ☆Ronny丶  阅读(12978)  评论(11编辑  收藏  举报

机器学习算法 原理、实现与实践 —— 感知机与梯度下降

 

一、前言

1,什么是神经网络?

人工神经网络(ANN)又称神经网络(NN),它是一种受生物学启发而产生的一种模拟人脑的学习系统。它通过相互连结的结点构成一个复杂的网络结构,每一个结点都具有多个输入和一个输出,并且该结点与其他结点以一个权重因子相连在一起。通俗来说,神经网络是一种学习器,给它一组输入,它会得到一组输出,神经网络里的结点相互连结决定了输入的数据在里面经过怎样的计算。我们可以通过大量的输入,让神经网络调整它自身的连接情况从而总是能够得到我们预期的输出。

2,神经网络能干什么吗?

神经网络对于逼近实数值、离散值或向量值的目标函数提供了一种健壮性很强的方法,现在已经成功应用到很多领域,例如视觉场景分析、手写字符识别、语音识别、人脸识别等。它可以适用在任何实例是“属性-值”$Feature \to Value$的情况中,需要学习的目标函数是定义在可以用向量描述的实例上,向量由预先定义的特征组成,比如字符识别,那么特征可以是图像中的每个像素亮度值。

现代计算机识别的过程通常都可以描述为这样一个过程:对象 $\to$特征$\to$类别,理论上都可以用神经网络来解决。

本篇文章将是人工神经网络这个主题的第一篇文章,主要从最简单的神经网络结构入手,介绍一些基本算法原理。后面会陆续涉及到多层神经网络的原理及其C++实现,同时随着深度学习的提出,卷积神经网络也渐渐走进人们的视野,它在图像识别上表现非常不错,这个主题也将在后面的文章中有全面的介绍。

二、感知器

一个感知器的结构图如下:

image

稍后我们会知道感知器实际上是神经网络结构中的一个神经元,那么一个感知器就够成了最简单的神经网络系统(虽然还算不上是网络)。感知器是以一组实数向量作为输入,计算这些输入的线性组合,如果结果大于某个阈值就输出1,否则就输出-1。如果我们用公式表达,则假设输入为$x_1$到$x_n$,那么感知器可以表示为一个函数:

$$ o(x)=\left\{
\begin{aligned}
1&\  if \ \ \ w_0+x_1w_1+\cdots+x_nw_n>0 \\ 
-1&\  otherwise
\end{aligned}
\right.
$$

我们把$-w_0$看作是上面提到的阈值,$w_1$至$w_n$为一组权值。$o(x)$就是将输入向量按一组权重进行线性加权求和后做的一个符号函数。

我们可以把感知器看成$n$维实例空间(点空间)中的超平面的决策面,平面一侧的所有实例输出1,对另一侧的实例输出-1,这个决策超平面的方程是$\vec{w} \cdot \vec{x}=0$。

针对于我们在数据分类的应用时,就是将我们提供的所有样本数据分为2类,对于其中一类样本,感知器总是输出1,而另一类总是输出-1。但是对于任意样本总能找出这个超平面吗,或者是说是找出一组这样的权值向量吗?答案显然是否定的,只有线性可分的空间可以找到超平面,或者说可以找出一组权值。

那么我们怎么利用感知器呢?或者说我们的目标是什么?

我们希望找到一组这样的权值,对于我们输入的每一组向量,总是能够得到一个我们期望的值。但是上面的感知器功能显然不够,它只能得到2个结果,即1和-1。

在实际的模式分类的应用中,样本空间往往并不是线性的,即使是2维数据的集合也可能不是线性可分的,比如下面这张图:

image

而用这样的感知器结点来构建神经网络显然是不行的,因为线性单元连结在一起得到的仍然是线性单元,我们需要的是一种非线性映射,于是就产生了激活函数这个概念。激活函数是一种非线性函数同时是可微函数(可以求导数),为什么要可微呢,因为我们需要知道权重是怎么影响最终输出的,我们要根据输出来调节那些权重向量,也就是后面讲到的梯度下降法则。

激活函数有很多种,关于激活函数的种类这里不准备介绍太多,只要知道我们选用的是S型激活函数,它将整个一维空间映射到[0,1]或[-1,1]。下面是S型$sigmoid$函数和它的导数:

$$
f(x)=\frac{1}{1+e^{-\alpha x}}(0<f(x)<1)
$$

$$
f'(x)=\frac{\alpha e^{-\alpha x}}{(1+e^{-\alpha x})^2}=\alpha f(x)[1-f(x)]
$$

经过这样的非线性映射,我们的感知器(现在应该叫SIMGOID单元)就变成了下面这种结构:

image

上面结构中$w_0$我们习惯称它为偏置,相当于我们多了一个$x_0=1$的输入。

对于上面这种结构,我们可以有如下结论:

1)对于任意一组输入和一个我们预想的在[0,1]之间的输出,我们总可以找到一组$\vec{w}$使得。

2)对于很多组这样的输入样本,我们可以通过不断的调整权值,来让它们的输出接近于我们预想的输出。

下面我们该考虑,如何求得这样的一组权值向量。

三、反向传播算法

我们需要在向量空间中搜索最合适的权值向量,但是我们不能盲目的搜索,需要有一定的规则指导我们的搜索,那么梯度下降就是很有用的方法。首先我们来定义输出误差,即对于任意一组权值向量,那它得到的输出和我们预想的输出之间的误差值。

定义误差的方法很多,不同的误差计算方法可以得到不同的权值更新法则,这里我们先用这样的定义:

$$
E(\vec{w})=\frac{1}{2}\sum_{d \in D}(t_d-o_d)^2
$$

上面公式中$D$代表了所有的输入实例,或者说是样本,$d$代表了一个样本实例,$o_d$表示感知器的输出,$t_d$代表我们预想的输出。

这样,我们的目标就明确了,就是想找到一组权值让这个误差的值最小,显然我们用误差对权值求导将是一个很好的选择,导数的意义是提供了一个方向,沿着这个方向改变权值,将会让总的误差变大,更形象的叫它为梯度。

$$
\nabla E(w_i)=\frac{\partial E}{\partial w}=\frac{1}{2}\frac{\partial \sum_{d \in D}(t_d-o_d)^2}{\partial w_i}=\frac{1}{2}\sum_{d \in D}\frac{\partial (t_d-o_d)^2}{\partial w_i}
$$

既然梯度确定了E最陡峭的上升的方向,那么梯度下降的训练法则是:

$$
\vec{w_i} \gets \vec{w_i}+\Delta \vec{w_i},其中\Delta \vec{w_i}=-\eta \frac{\partial E}{\partial w_i}
$$

梯度下降是一种重要最优化算法,但在应用它的时候通常会有两个问题:

1)有时收敛过程可能非常慢;

2)如果误差曲面上有多个局极小值,那么不能保证这个过程会找到全局最小值。

为了解决上面的问题,实际中我们应用的是梯度下降的一种变体被称为随机梯度下降。上面公式中的误差是针对于所有训练样本而得到的,而随机梯度下降的思想是根据每个单独的训练样本来更新权值,这样我们上面的梯度公式就变成了:

$$
\frac{\partial E}{\partial w_i}=\frac{1}{2}\frac{\partial (t-o)^2}{\partial w_i}=-(t-o)\frac{\partial o}{\partial w_i}
$$

经过推导后,我们就可以得到最终的权值更新的公式:

$$
w_i=w_i+\Delta w_i \\
\delta=(t-o)o(1-o) \\
\Delta w_i=\eta \delta x_i
$$

有了上面权重的更新公式后,我们就可以通过输入大量的实例样本,来根据我们预期的结果不断地调整权值,从而最终得到一组权值使得我们的SIGMOID能够对一个新的样本输入得到正确的或无限接近的结果。

四、实例说明

上面我们已经介绍了经过基本的感知器,我们构造了一种SIGMOID单元,可以对“输入向量-值”这种模式的数据进行目标函数的逼近,但是这毕竟只是单个神经元,它逼近不了太复杂的映射关系,我们需要构造一个多层的神经网络结构来解决更一般的学习与分类问题。

下面我们通过一个简单的逼近实例来说明单个SIMGOID单元的工作原理。

首先,我们假设我们的输入是一个4维的向量$x=[x_1,x_2,x_3,x_4]$,其中$x_i$的值为0或者1。为了简单其见,我们只设计了下面4种样本。

$$
x_1=[1,0 , 0,0]\\
x_2=[0,1 , 0,0]\\
x_3=[0,0 , 1,0]\\
x_4=[0,0 , 0,1]\\
$$

对于这4类样本,我们希望它们得到4种不同的结果以说明它们属于哪一种,也就是我们的目标输出是一个标号,像下面这样:

$$
x_1 \to 1 \\
x_2 \to 2 \\
x_3 \to 3 \\
x_4 \to 4 \\
$$

上面的样本只是4种,我们可以让每一种样本重复来构建大量的样本实例。比如实际采集到的样本可能会有所浮动,比如与$x_1$同类的样本可能采集到的数据是这样的$x=[0.993,0.002 , 0.0012,-0.019]$,所以我们可以用很小的随机数来模拟大量的样本输入。

因为我们的SIMGOID单元输出值只可能是$[0,1]$,所以我们可以将我们的类别标号归一化为$[1,2,3,4]/4$,下面我们用C++来模拟这一过程。

1,样本获取:

 1 void SampleNN::getSamplesData()
 2 {
 3     const int iterations = 15000; // 15000个样本
 4     for (int i = 0; i < iterations; i++)
 5     {
 6         int index = i % 4;
 7         vector<double> dvect(4, 0);
 8         dvect[index] = 1;
 9         for (size_t i = 0; i != dvect.size(); i++)
10         {
11             dvect[i] += (5e-3*rand() / RAND_MAX - 2.5e-3);
12         }
13         inputData.push_back(dvect);
14     }
15 }

2,用[0,0.05]之间的随机值初始化权重。

1 void SampleNN::intialWgt()
2 {
3     // 4个连结和一个偏置w0
4     for (int i = 0; i != 5; i++)
5     {
6         weight.push_back(0.05*rand()/RAND_MAX);
7     }
8 }

3,向前计算

1 void SampleNN::cmtForward(const vector<double>& inVect)
2 {
3     double dsum = weight[4];//先把偏置加上
4     for (size_t i = 0; i != inVect.size(); i++)
5     {
6         dsum += (inVect[i] * weight[i]);
7     }
8     actual_output = 1 / (1 + exp(-1*dsum));
9 }

4,更新权重

 1 void SampleNN::updataWgt(const vector<double>& inVect,const double true_output)
 2 {
 3     double learnRate = 0.05; // 权重更新参数
 4     for (size_t i = 0; i != weight.size() - 1; i++)
 5     {
 6         weight[i] += (learnRate*(true_output - actual_output)*actual_output*(1 - actual_output)*inVect[i]);
 7     }
 8     // w0单独计算
 9     weight[4] += (learnRate*(true_output - actual_output)*actual_output*(1 - actual_output)*1);
10 }

下面是经过15000次迭代后得到的结果:

image

从上面结果可以看出,输出的值基本收敛于0.25、0.5、0.75与0.96,说明已经可以用来分类的了。

五、结束语

经过上面的讨论,单个神经元的功能及其原理应该可以清楚的了解,那么下一步,我们将用这些单个的神经元(SIGMOID单元)相互连结组成一个网状结构形成神经网络,并用来做更一些有意义的识别,这些内容将在下一篇文章中详细描述。