zoukankan      html  css  js  c++  java
  • 感知机

    模型

    机器学习中,感知机(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;
            }
  • 相关阅读:
    JB的IDE可视化MongoDB、MySQL数据库信息
    爬取QQ音乐(讲解爬虫思路)
    selenium实现淘宝的商品爬取
    爬取中国福彩网并做可视化分析
    Django的学习进阶(二)———— name
    xpath获取一个标签下的多个同级标签
    selenium中动作链的使用
    Error: Cannot find module 'electron-prebuilt'
    error Invalid tag name "–save-dev": Tags may not have any characters that encodeURIComponent encodes
    TypeError:mainWindow.loadUrl is not a function
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/6834520.html
Copyright © 2011-2022 走看看