感知机

模型

机器学习中,感知机(perceptron)是一种二值监督学习的算法(相应的函数,能决定输入是否属于某个分类,其中输入由一个数值向量表示)。感知机是一个线性分类器,其分类函数基于线性预测函数对新的输入作出预测,即,将输入(实数向量)映射到{-1,1}二值空间:

f(x)=sign(wx+b)

其中,sign是符号函数

wx是w向量与x向量的内积,w向量与x向量均为n维,分别为如下形式

w=[w1,w2,...,wn]

x=[x1,x2,...,xn]T

(也许会疑问,怎么知道f(x)就是这种形式的呢? ...  因为感知机就是定义为线性分类,自然分类函数f(x)就可以写成上面那种形式,当然对于数据集可以采用其他非线性分类的方法,不过这就不是这里的感知机的工作了 -_-!!)

不难发现,f(x)是n+1维欧氏空间的超平面,n维空间中的点被f(x)分为两部分(即两类),为了方便计算和表示,我们增加一个维度,使得b = w0x0, 其中x0=1,并且将w也写成列向量形式,则有,

其中,w=[w0,w1,w2,...,wn]T

感知机学习的目的就是要找到一个perceptron,能把不同类别的点区分开来,具体来说就是,由训练数据集

T={(x1,y1), (x2,y2), ... , (xN, yN)}

其中N表示有N个数据,xi属于n+1维实数空间向量,yi属于{1, -1},求n+1维向量w

 当然,这里需要有一个前提条件,训练数据要线性可分(比如如下图中的数据就不能线性可分)

 (n+1=3,假设圈圈表示y值为1,叉叉表示y值为-1,图中三点一线,线性不可分)

线性可分是指:

如果存在某个超平面wTx=0,对数据集中所有yi=1的数据,wTxi>0,对所有yi=-1的数据,wTxi<0,则这个数据集线性可分

学习策略

定义(经验)损失函数(参数为w),并设法将损失函数极小化,比如损失函数为误分类点的个数,然后极小化为0,则就是找到一个符合要求的超平面。巴特,这样的损失函数不是关于w连续可导,不容易找。

如果选择损失函数为误分类点到超平面的总距离,那么当误分类点为0时,总距离也为0,分类正确。

输入空间一点x到超平面的距离为

 其中,||w||为w的L2范数,然后,对于误分类点来说,距离则为

 因为对于误分类点,y值与wTx符号相反,所以需要加个负号,股损失函数为误分类点总距离,假设误分类点集合为SetM,则,

对于上式右边括号中的部分,我们可以认为分母是为了让wT归一化,而我们目的也是为了让SetM最终为空集合,从而使L为0,所以这里分母并没有什么影响,为了简单起见,这里将分母去掉,

其中,SetM是误分类点集合,xi为n+1维列向量,w为n+1维列向量,yi为{1,-1}中的某个标量值,可以看出,误分类点越少,L越小,误分类点离超平面越近,L越小。当SetM为空集合(没有误分类点)时,L = 0,存在

误分类点时,在误分类点一定的情况下,L是w的线性函数。

学习算法

 采用随机梯度下降法,这是因为只有误分类的点参与优化w,优化过程中,一次选择一个误分类点使其梯度下降。

损失函数对w的偏导数为

这是一个与xi一样的n维列向量,则针对某一个误分类点的梯度下降的优化为,

w← w + ayixi                                 (*)

其中 + 号表示梯度下降,a表示步长,范围为(0,1],yi为第i个点的分类值(1,或者-1)。

还记得前面说的那个超平面的截距b吗?b其实就是这里的w0,因为x0=1,故

b←b + ayixi0 = b + ayi

当然了,这里b作为w的第0个分量,在计算表达式时没必要单独列出来优化。

算法:

1. 原始形式

输入:训练数据集{(x1,y1), (x2,y2), ... , (xN, yN)},对某个数据点(xi,yi),检测条件 yiwTxi > 0 是否满足,如果不满足表示是误分类点,则进行优化

输出:w 值

  1. 选择w初值,比如n+1维零向量[0,0, ... , 0]T ,,步长a选择1
  2. 遍历训练集:

    a) 当(xi, yi), yiwTxi <= 0,则使用上面的(*)式对w进行优化,注意,xi的第0维值始终为1

    b) 如果优化后依然yiwTxi <= 0,则继续a)步骤,直到 yiwTxi >0, 设置标志位flag 为true,表示有误差点

  3. 继续check (xi+1,yi+1), 如果不满足检测条件,则跳到2.a)步骤进行优化,否则继续检测 i + 2 的数据点。如此一直到训练集遍历结束
  4. 检测flag是否为true,如果为true,则 重置标志位为false,并跳至2步骤执行,否则退出

由于实际应用中,往往训练数据的x 是n维,可以视情况决定是否将b 独立出来优化计算。

2. 对偶形式

根据上面的(*)式,我们知道在最后计算完成后,w变为

(上式中i从i=1开始)其中ni为第i个数据总共参与优化的次数,如果将截距b单独写出来(对任意的xi,其第0维值为1),则b为:

可以假设w初值为零向量,则w可写为

  (1)

(上式中i从i=1开始)对应的b则为

其中xi为第i个数据点(n+1维列向量),yi为第i个数据点的分类值(1或者-1),a为步长(标量),ni为整个优化过程中,使用第i个数据点进行优化的次数

根据上面对原始形式的分析,我们知道,优化是检测每一个数据点是否符合检测条件 yiwTxi > 0, 带入w的表达式,检测条件变为:

   (2)

其中j表示N个数据集中第j个数据,xjTxi为向量内积,可以在一开始的时候计算,这样就得到一个所有内积的N*N的矩阵,这个就是Gram矩阵,从而避免在后面每一次循环的时候都要计算,从而节省时间,这也是对偶形式对原始形式所做的改进(原始形式中每次循环中wTxi就是做内积计算)。

总结步骤如下:

  1. 选择步长a为1,nj均为0
  2. 遍历训练集,
    a) 当(xi, yi),不满足检测条件,则作优化,ni ← (ni+1)a = ni + 1,并设置标志位flag = true,表示此轮有误分类点
    b) 然后重新检测条件是否满足,如不满足,跳至 a) 继续优化,否则跳到步骤3
  3. 检测下一个数据点(xi+1,yi+1) 是否满足条件,如不满足,跳至 2.a) 步骤,否则跳至 步骤4
  4. 检测标志位,如果为true,则reset flag = false,并跳至步骤2,否则,退出

需要注意的是,上面计算矩阵时,向量的内积是n+1维的,其中第0维值为定值1,这就导致Gram矩阵中每个元素值比n维时大1(注意,维度不变,仍然是N*N),这是为了方便表达才写成n+1维,如果实际计算中不方便这么处理,可以写成n维,那么检测条件变为

优化时,ni ← (ni+1)a = ni + 1,而 b ← (ni+1)yi,可以看出对所有N个数据点累加b的变化正好符合上面给出的式子。

 补充说明

 下面稍微证明一下优化是有效的,而不是碰运气。

假设wf是能够满足要求的,也就是说,对于数据集中任意一个数据点(xi,yi),都满足上面的检测条件yiwfTxi > 0,那么最小值也大于0, min(ywfTx) > 0,因为w是向量,所有如果当前的w与wf的夹角变小,说明优化是有效的

对于某一次使用数据点(xi,yi)优化后,wt 变成了wt+1,计算向量的内积

但是内积增大,不一定是说夹角变小,也许是wt+1的长度变大了呢?

 考虑w的L2范数的平方(平方是为了计算方便)

 

注意上式中第一行最右端的第二项值小于等于0,这是因为使用的误差点不满足检测条件。

在给定训练数据集和步长a之后,上式第二行最右边的max(..)子项值是固定的,也就是说优化w,就算使其长度增加,也是有上限的。

现在,假设一开始w初值为0向量w0,经过t次优化为变成wt,那么,

 

且有

故而wt与wf的夹角余弦为,

其中,

 

易知,这两个参数在给定步长a和数据集的情况下均为定值且大于0,注意到,因为wf下,所有数据点均符合检测条件,

所以ρ >0,且其中 wf被分母归一化,假设归一化后的wf为wfs,则wfs的为n+1维空间的单位向量,

而R 自然是大于0,因为不可能数据集中所有点均为零向量。

因为夹角余弦不超过1,所以

从而

即,需要优化的步骤数t的上限。

 Pocket Step

有时候数据集数量N特别大,或者数据集不是线性可分但是事先我们并不知道,这种时候导致优化将会消费大量时间,或者无法停止。这时,我们可以加入pocket step,也就是每次迭代优化w后,

重新使用原始数据集计算误分类次数(此时每次计算时不对w进行优化),并记录此次优化后的误分类次数,然后再对下一个数据点检测,并在需要的时候做优化,每次做完优化后就对原始数据集

统计误分类次数,当迭代优化T次后,就有对应的T个误分类次数,选择误分类次数最少的那个w。当N足够大的时候,所得到的w还算差强人意。

ref: 

  1. 《统计学习方法》,李航
  2. http://beader.me/2013/12/21/perceptron-learning-algorithm/

 示例代码

(仅作帮助理解用,未经测试)

感知机类

    public class Perceptron
    {
        /// <summary>
        /// 第一项为b,对应的x0为1
        /// </summary>
        public double[] w;
        /// <summary>
        /// 学习
        /// </summary>
        /// <param name="data"></param>
        public void Learn(PData data)
        {
            w = new double[data.J];
            var a = 1;                  // 这里简单起见,hardcode步长

            bool flag = true;           // 优化标志位,初始化为true
            while(flag)
            {
                flag = false;       // 标志位复位
                for(int i = 0; i < data.matrix.GetLength(0); i++)
                {
                    var row = data.matrix[i];       // 一行表示一条数据
                    var dist = PUtil.CalcDistance(row, w);
                    while(dist <= 0)
                    {
                        flag = true;        // 表示进行过优化
                        PUtil.Calibrate(w, row, a);
                        dist = PUtil.CalcDistance(row, w);
                    }
                }
            }
        }
        /// <summary>
        /// 根据输入判断输出,输出为-1或1
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public double Judge(double[] input)
        {
            double sum = 0;
            for(int i = 0; i < w.Length; i++)
            {
                if (i == 0)
                    sum += w[i];
                else
                    sum += w[i] * input[i - 1];
            }
            return sum >= 0 ? 1 : -1;
        }
    }

帮助类

    public class PUtil
    {
        /// <summary>
        /// 计算点与超平面距离
        /// </summary>
        /// <param name="row">数据点</param>
        /// <param name="w"></param>
        /// <returns></returns>
        public static double CalcDistance(double[] row, double[] w)
        {
            var y = row[row.Length - 1];
            double sum = 0;
            for(int i = 0; i < w.Length; i++)
            {
                if (i == 0)
                    sum += w[0];
                else
                    sum += w[i] * row[i - 1];
            }
            return y * sum;
        }
        /// <summary>
        /// 校正
        /// </summary>
        /// <param name="w">权重</param>
        /// <param name="row">数据点</param>
        /// <param name="a">步长</param>
        public static void Calibrate(double[] w, double[] row, double a)
        {
            var f = row[row.Length - 1] * a;
            for(int i = 0; i < w.Length; i++)
            {
                if (i == 0)
                    w[i] += f;
                else
                    w[i] += row[i - 1];
            }
        }
    }

 如果采用对偶形式的学习策略,则上面Learn方法修改为

        /// <summary>
        /// 对偶形式的学习
        /// </summary>
        /// <param name="data"></param>
        public void Learn_(PData data)
        {
            w = new double[data.J];         // 截距 b 作为 w的第一项
            var N = data.matrix.GetLength(0);       // 数据量
            var gram = PUtil.GetGram(data.matrix);      // 数据点输入的对偶内积矩阵
            var n = new int[N];                     // 每个数据点参与优化的次数
            double a = 1;                       // 步长,hardcode 设置为 1
            bool flag = true;
            while(flag)
            {
                flag = false;
                // 遍历数据点
                for(int i = 0; i < N; i++)
                {
                    var check = PUtil.Check_(data.matrix, i, gram, n, a);
                    while(check < 0)
                    {
                        flag = true;            // 本轮进行了优化
                        n[i] += 1;              // 第 i 个数据点参与优化次数增 1

                        check = PUtil.Check_(data.matrix, i, gram, n, a);
                    }
                }
            }

            // 设置 w 的值, 根据(1)式
            for(int j = 0; j < data.J; j ++)
            {
                for(int k = 0; k < N; k++)
                {
                    if (j == 0)
                        w[j] += n[k] * a * data.matrix[k][data.J - 1];
                    else
                        w[j] += n[k] * a * data.matrix[k][data.J - 1] * data.matrix[k][j - 1];
                }
            }
        }

对应的辅助方法为

        /// <summary>
        /// 获取数据输入的对偶内积矩阵
        /// </summary>
        /// <param name="matrix"></param>
        /// <returns></returns>
        public static double[][] GetGram(double[][] matrix)
        {
            var N = matrix.GetLength(0);
            var J = matrix.GetLength(1);
            var gram = new double[N][];

            for(int i = 0; i < N; i++)
            {
                gram[i] = new double[N];
                for(int k = 0; k < N; k++)
                {
                    double sum = 0;
                    for(int j = 0; j < J; j++)
                    {
                        if (j == 0)
                            sum += 1;
                        else
                            sum += matrix[i][j - 1] * matrix[k][j - 1];
                    }
                    gram[i][k] = sum;
                }
            }
            return gram;
        }
        /// <summary>
        /// 对偶形式的检测条件
        /// </summary>
        /// <param name="matrix"></param>
        /// <param name="i"></param>
        /// <param name="gram"></param>
        /// <param name="n"></param>
        /// <param name="a"></param>
        /// <returns></returns>
        public static double Check_(double[][] matrix, int i, double[][] gram, int[] n, double a)
        {
            var N = matrix.GetLength(0);    // 样本数据量
            var J = matrix.GetLength(1);   // 数据维度
            var xy = matrix[i];             // 当前检测的数据点
            var multiply = gram[i];         // 关联 i 数据点的 内积向量

            double sigma = 0;
            for(int k = 0; k < N; k++)
            {
                sigma += n[k] * a * matrix[k][J - 1] * multiply[k];     // (2)式
            }
            return matrix[i][J - 1] * sigma;
        }
原文地址:https://www.cnblogs.com/sjjsxl/p/6834520.html