从软件工程的角度写机器学习5——SVM(支持向量机)实现

2023-11-14

SVM实现

SVM在浅层学习时代是主流监督学习算法,在深度学习时代也往往作为最后一个预测层使用(说深度学习击败了SVM的纯属扯淡)。

SVM算法总体流程

本系列文章旨在讲解机器学习算法的工程实现方法,不在于推导数学原理。因此想深入了解原理的请移步去看《支持向量机通俗导论(理解SVM的三层境界)》:
http://www.cnblogs.com/v-July-v/archive/2012/06/01/2539022.html

对于急于求成的小伙伴,建议就看下面描述的基本过程,知其然,不必知其所以然。

按照上篇文章所规定的工程框架,所有操作尽量以矩阵为单元执行:
http://blog.csdn.net/jxt1234and2010/article/details/51926758

预测

SVM是一个监督学习算法,其训练时输入自变量矩阵X(N个m维向量)和因变量矩阵Y(这里要求Y中元素取值为两类之一:{A、B}),输出模型M。

SVM训练
如图所示,一个SVM模型包含如下要素:
1、支撑向量集SV
一个n行m列的矩阵,表示n个m维向量。
支撑向量集是在训练过程中,挑选出自变量矩阵的有效行形成的。
2、系数C与截距b
系数矩阵为n行1列的矩阵,它与支撑向量集一一对应。
截距为一个常数。
3、核函数K
核函数为实现了如下接口的一个类

class IKernel
{
public:
    virtual Matrix* vCompute(const Matrix* X1, const Matrix* X2) const = 0;
};

其中X1为 [m, n1]的矩阵,X2为[m, n2]的矩阵,输出结果为 [n2, n1] 的矩阵。

具体的核函数运算参考:
http://blog.csdn.net/xiaowei_cqu/article/details/35993729

预测时输入自变量矩阵X(待预测向量集)和模型M,输出预测值矩阵YP。
X为[m,p]的矩阵,表示p个待预测的m维向量。
YP为[1,p]的矩阵,表示p个预测值。

SVM预测

第一步,由核函数算出SV和X的核内积矩阵KX。KX(i, j)即第i个支撑向量和第j个待预测向量的核内积。
第二步,用KX和对应的系数矩阵相乘,加上截距,得到YP。

预值出来的YP只是一个实数,不表示具体类别,使用时需要对每个值作类别转化,大于0转为类别A,小于0转为类别B。

预测过程可以用一个公式简单表示:

YP=K((M.SV), X)∙(M.C)+B

训练过程

在SVM模型中,核函数是事先指定的,支撑向量是从自变量向量集中抽取的,因此训练过程主要就是计算系数矩阵和截矩。

因变量矩阵Y假定其元素只有A、B两个取值
训练之前先要预处理,将因变量矩阵Y中A转为1,B转为-1。

SVM训练细分
如图所示,SVM模型的创建分三步:
1、创建二次规划(主要是计算核内积矩阵)
2、解二次规划,得到截矩b和原始系数矩阵A,原始系数矩阵为[1,N]的矩阵,其系数值与自变量矩阵中的向量一一对应。
3、把原始系数矩阵中非零系数及对应的自变量矩阵中的向量挑选出来,形成系数矩阵C及其对应的支撑向量集SV。

第3步只是一个简单的挑选过程,因此经常被一带而过,往往只有自己去实现,才能记得。

熟悉SVM的朋友可以看出,按上面这个流程这个方式写出来的SVM,并没有交叉验证、核函数选择及参数调优及多分类的功能。主要是考虑到这些特性并非SVM专属,应当在算法抽象层实现,以便给其他监督学习算法共享。

基础算法实现及相应的类

训练类

SVM是一种监督学习算法,对应的训练类定义为SVMLearner

class SVMLearner
{
public:
    virtual IPredictor* vLearn(Matrix* X, Matrix* Y) const;
private:
    double mG;//对应松驰因子G
    IKernel* mK;//核函数
    ISVMProgramSolver* mSolver;//二次规划求解器
};

二次规划生成

规划描述

mina12ijyiyjkijaiajiai

0<=ai<=G,iaiyi=0

ai 表示系数矩阵A的第i行的值
yi 表示因变量矩阵Y的第i行的值
kij 表示核内积矩阵第i行第j列的值,即第i个自变量向量和第j个自变量向量的核内积。
G为松驰因子,事先设定。

截距b值由如下公式推断:

b=iyiaikikyk,ak>0

至于,为什么是这个二次规划公式,请移步相关的原理文章看推导过程。

看完上面的结构图,我们定义如下的结构体去描述SVM创建的二次规划:

struct SVMProblem
{
    Matrix* K;
    Matrix* Y;
    double G;

    /*解的描述:系数矩阵及截矩*/
    struct Answer{
        Matrix* C;
        double b;
    };
};

这里并不定义一个通用的二次规划问题结构体,主要考虑到仅SVM算法涉及二次规划的求解,不是太有必要基于普通二次规划求解。

这个结构体中,松驰变量G需要事先设定,Y直接来源于因变量,因此,创建二次规划的过程就等同于核函数运算的过程,即求出核内积矩阵: K=K(X,X)

核函数运算

构造二次规划问题过程中,最核心的是核函数运算,即求出所有 kij
核函数本身是向量间的一种内积,但为了后续优化方便,我们将核函数的接口定义为对矩阵的运算。

考虑到核函数有多种,定义相应的类:
比如线性核函数:

class LinearKernel:public IKernel
{
public:
    virtual Matrix* vCompute(const Matrix* X1, const Matrix* X2) const;
};

径向基核函数:

class RBFKernel:public IKernel
{
public:
    virtual Matrix* vCompute(const Matrix* X1, const Matrix* X2) const;

private:
    double mGamma;
};

注意到核内积矩阵K是[N,N]的规模,当训练数据量很大时,这个矩阵是非常大的。
核函数计算还是相当好并行化实现的,不过对于不同的核函数,似乎没有一个统一的方案,只好一个个来优化了。

二次规划求解

SVM的二次规划的求解是一种凸优化问题,能求出导数表达式,因此可以用通用的优化算法梯度下降法求解。此外,由于它本身的特殊性,可以用一种更快的优化方法SMO算法。

考虑到有多种解法,在这定义一个接口:

class ISVMProgramSolver {
public:
    virtual SVMProblem::Answer* vSolve(Problem*) const = 0;
};
梯度下降

定义梯度下降法解SVMProblem的类为
GradientDecentSVMProgramSolver

class GradientDecentSVMProgramSolver:public ISVMProgramSolver {
public:
    virtual SVMProblem::Answer* vSolve(Problem*) const;
};

梯度下降是一种通用性质的优化算法了,有必要定义梯度下降这一优化算法抽象出来。
梯度下降简单来说就是一个公式:

θ=θαθJ

θ 为待确定的系数向量, α 为一个系数, θJ 为目标函数对系数向量的偏导。

求偏导这种工作涉及符号代数,需要加公式解析和公式树运算的模块,本工程就不做这个事情了,而是直接将梯度函数定义为一个接口,每个上层算法自行实现:

class DerivativeFunction {
public:
    /*输入:初始的系数向量 coefficient,用于更新系数的样本集矩阵X
      输出:系数梯度向量,与 coefficient 同长
    */
    virtual Matrix* vCompute(Matrix* coefficient, Matrix* X) const = 0;
};

梯度下降算法定义为如下的接口:
其中系数矩阵 coefficient 既是输入(需要提供初始值),也是输出,X为样本集,alpha为更新速度,iteration为最高迭代次数,DerivativeFunction为梯度计算函数。

class IGradientDecent {
public:
    virtual void vOptimize(Matrix* coefficient, Matrix* X, const DerivativeFunction* delta, double alpha, int iteration) const = 0;
};

其中可分两种实现方式:

/*普通梯度下降,每次用全部样本来更新梯度*/
class GradientDecent :public IGradientDecent{
public:
    virtual void vOptimize(Matrix* coefficient, Matrix* X, const DerivativeFunction* delta, double alpha, int iteration) const;
};

/*随机梯度下降,每次随机抽取样本来更新梯度*/
class StochasticGradientDecent : public IGradientDecent{
public:
    virtual void vOptimize(Matrix* coefficient, Matrix* X, const DerivativeFunction* delta, double alpha, int iteration) const;
};

在GradientDecentSVMProgramSolver的实现中,
定义一个 DerivativeFunction,初始化一个全为零的coefficient矩阵,然后由通用的梯度下降算法去确定系数即可。

SMO

http://www.cnblogs.com/jerrylead/archive/2011/03/18/1988419.html
SMO是一个相对专一的优化算法,因此直接写为一个继承ISVMProgramSolver的类。

class SMOSVMProgramSolver:public ISVMProgramSolver {
public:
    virtual SVMProblem::Answer* vSolve(Problem*) const;
};

预测类

定义SVM的预测类如下:

/*预测模型*/
class SVMPredictor:public IPredictor
{
public:
    //获得类别值
    virtual Matrix* vPredict(Matrix* X) const;

    //获得各个类别的概率([0,1]区间)
    virtual Matrix* vPredictProb(Matrix* X) const;

    //获得各个类型的值
    virtual Matrix* vGetValues() const;

    //保存为树结点(后续可将树结点存成json)
    virtual Node* vDump() const;
private:
    double mTypeA;//A类别的值
    double mTypeB;//B类别的值
    std::string mKernelDescription;//核函数描述,根据此字段可以重建核函数
    IKernel* mKernel;//核函数
    Matrix* mSV;//支持向量
    double mB;//截矩
};

抽象特性实现

讲SVM的文章,实现SVM的代码都把这些特性内嵌到SVM算法本身去了。但这个原应在一个更高的抽象层实现。现在,让我们仅仅将SVM视为一个普通的监督学习算法,实现这些特性。

交叉验证

关于交叉验证的说明参考如下文章:
http://blog.csdn.net/xywlpo/article/details/6531128

在工程中,交叉验证是一个监督学习的评价类:

/*CRV Means Cross Validate*/
class ICRVEvaluate {
public:
    //ICompare为对比函数,依据预测值和真实值的差距评分,一般取为n阶矩离
    class ICompare {
    public:
        virtual double vCompare(Matrix* Y, Matrix* YP) const;
    };
    virtual double vEvaluate(ILearner* learner, Matrix* X, Matrix* Y, ICompare* compare) const = 0;
};

交叉验证有若干种方式,但统一的流程都是拆分——训练——预测与结果累加,以k-folder cross-validation为例:

class KFolderCrossValidation : public ICRVEvaluate {
public:
    virtual double vEvaluate(ILearner* learner, Matrix* X, Matrix* Y, ICompare* compare) const {
        double sumValue = 0.0;
        for (int i=0; i<mK; ++i) {
            Matrix* trainSetX = splitForTrain(X, i);
            Matrix* trainSetY = splitForTrain(X, i);
            IPredictor* p = learner->vLearn(subSetX, subSet, Y);
            delete trainSetX;
            delete trainSetY;
            Matrix* testSetX = splitForTest(X, i);
            Matrix* predictY = p->vPredict(testSetX);
            delete testSetX;
            delete p;

            Matrix* testSetY = splitForTest(Y, i);
            sumValue += compare->vCompare(predictY, testSetY);
            delete testSetY;
            delete predictY;
        }
        return sumValue;
    }
private:
    int mK;//交叉验证的划分数,一般取10
};

选择了交叉验证函数之后,可以此去判定一个监督学习算法的好坏,这个就是后续参数调优中的目标函数。

核函数选择与参数调优

SVM的核函数选择与参数调优无法基于数理统计找到适合的优化方法,只能使用启发式优化算法或穷举。这种优化算法理应独自为一通用模块:
定义一个通用的启发式优化算法:

class IOptimizer{
public:
    /*评价函数*/
    class IEvaluate {
    public:
        //parameters 为[n,1]的矩阵,即一个向量,其中每个数都为[0,1]的实数
        virtual double vEvaluate(Matrix* parameters) const = 0;
    };

    /*parameters既是输入也是输出,evaluate为评价函数*/
    virtual void vOptimize(Matrix* parameters, IEvaluate* evaluate) const = 0;
};

定义一个SVMFactory,基于parameters构建SVM训练器,此处类似于抽象工厂的设计:

class ILearnerFactory{
public:
    virtual ILearner* vCreate(Matrix* parameters) const = 0;
};

class SVMFactory : public ILearnerFactory{
public:
    virtual ILearner* vCreate(Matrix* parameters) const;
};

这个构建过程将挑选SVMLearner中的Kernel函数和二次规划求解函数并确定G值。

针对监督学习算法的评价函数便可如此编写:

/*评价函数*/
class LearnerEvaluate:public IOptimizer::IEvaluate {
public:
    virtual double vEvaluate(Matrix* parameters) const {
        ILearner* l = mFactory->vCreate(parameters);
        double p = mEvaluate->vEvaluate(l, mX, mY, mCompare);
        delete l;
        return p;
    }
private:
    ILearnerFactory* mFactory;
    ICRVEvaluate* mEvaluate;
    ICRVEvaluate::ICompare* mCompare;
    Matrix* mX;
    Matrix* mY;
};

实际的优化算法可使用遗传算法、粒子群等。

//遗传算法
class GAOptimizer : public IOptimizer{
public:
    virtual void vOptimize(Matrix* parameters, IEvaluate* evaluate) const;
};

//粒子群算法
class PSOOptimizer : public IOptimizer{
public:
    virtual void vOptimize(Matrix* parameters, IEvaluate* evaluate) const;
};

定义引入参数调优的监督学习算法类:

class OptimizedLearner:public ILearner {
public:
    virtual IPredictor* vLearn(Matrix* X, Matrix* Y) const;
private:
    ILearnerFactory* mFactory;
    IOptimizer* mOptimizer;
    int mParameterSize;
};

该类将先用参数寻优算法构造一个实际的训练器ILearner,再用该训练器得到模型。

多分类实现

SVM原理上只能进行二分类,为了支持多分类,需要将其扩展,如何扩展参考这篇文章:
http://www.cnblogs.com/CheeseZH/p/5265959.html

如上面的文章所述,有多种扩展方式,我们定义如下几个装饰类:

/*一对多法*/
class OVRLearner : public ILearner{
public:
    OVRLearner(ILearner* l);
    virtual IPredictor* vLearn(Matrix* X, Matrix* Y) const;
private:
    ILearner* mOrigin;
};

/*一对一法*/
class OVOLearner : public ILearner{
public:
    OVOLearner(ILearner* l);
    virtual IPredictor* vLearn(Matrix* X, Matrix* Y) const;
private:
    ILearner* mOrigin;
};

创建多分类器时,基于二分类器包裹一下即可,比如:

ILearner* origin = factory->vCreate(parameters);
ILearner* l = new OVOLearner(origin);

抽象层的各种Learner对应地需要定义各自的Predictor,不详述。

代码框架

比起上一节的决策树,SVM的代码框架明显复杂了许多,主要其中很大一部分是可复用的,独立出来以便其他算法调用:
SVM代码框架

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从软件工程的角度写机器学习5——SVM(支持向量机)实现 的相关文章

随机推荐

  • 【面试宝典】美团二面:Redis与MySQL双写一致性如何保证?

    前言 四月份的时候 有位好朋友去美团面试 他说 被问到Redis与MySQL双写一致性如何保证 这道题其实就是在问缓存和数据库在双写场景下 一致性是如何保证的 本文将跟大家一起来探讨如何回答这个问题 谈谈一致性 一致性就是数据保持一致 在分
  • VSCODE显示服务器输出的图片

    1 使用matplotlib库 import matplotlib pyplot as plt plt 用于显示图片 import matplotlib image as mpimg mpimg 用于读取图片 import numpy as
  • 一个 8 年 PhpStorm 使用者的配置分享

    我使用 PhpStorm 很长时间了 差不多 8 年 更准确地说是从 2012 年开始 那时候是第三版 那段时间发生了许多事 也发生了很大的改变 当然 你每天都会学到很多 这篇文章是我在 PhpStorm 的 8 年经验总结 我的这些最佳设
  • 交互式前景提取使用GrabCut算法(opencv_python学习)

    交互式前景提取使用GrabCut算法 cv grabCut 是 OpenCV 中用于执行 GrabCut 算法的函数 该函数可以将输入图像分割为前景和背景 下面是 cv grabCut 函数的基本语法 cv grabCut img mask
  • 模拟器显示图片,而真机不显示

    记录一个小bug 图片能在模拟器显示 但是在真机上显示不了 原因 图片的url有问题 真机有安全性限制 导致无法展示 1 首先 拿到图片的地址 将其拿到浏览器测试 可以看到浏览器显示的不安全 http www xxxxx com 9000
  • git最简单回滚并推送到远程

    1 代码回退 首先你要用git reflog查看你要回到的那个版本 然后用 git reset hard HEAD 回退到上个版本 git reset hard commit id 退到 进到 指定commit id 来把你的本地代码回到你
  • UDP与TCP的对比

    1 报头 1 TCP协议报头 TCP指传输控制协议 其报头格式如下 1 源 目的端口号 表示数据是从哪个进程来 到哪个进程去 2 32位序号 32确认号 用于可靠传输 3 4位TCP报头长度 表示该TCP头部有多少个32位bit 有多少个4
  • 忽视日志吃大亏,手把手教你玩转 SpringBoot 日志!

    一 日志重要吗 程序中的日志重要吗 在回答这个问题前 笔者先说个事例 笔者印象尤深的就是去年某个同事 收到了客户反馈的紧急bug 尽管申请到了日志文件 但因为很多关键步骤没有打印日志 导致排查进度很慢 数个小时都没能排查到问题 也无法给出解
  • Flutter 3.3 正式发布

    Flutter 3 是我们正式为全平台提供支持的一个重量级里程碑 距离它的发布仅过去了三个月 今天让我们有请 Flutter 3 3 正式版 近三个月我们并没有放慢更新迭代的速度 自 Flutter 3 发布以来 我们已经为 Flutter
  • 如何不借助新的变量交换两个变量的值

    在很多问题的解决中都会遇到 需要交换两个变量的值 这种情况 下面的方法 三变量法 想必是大家常用的吧 include
  • 【数据结构与算法】内排序算法全解析(附C语言代码)

    导览 0 预备知识 0 1 排序的概念 0 2 排序的稳定性 0 3 内排序与外排序 0 4 排序算法的性能 0 5 常见排序算法的性能 1 比较排序 1 1 插入排序 1 1 1 直接插入排序 1 1 2 折半插入排序 1 1 3 希尔排
  • Redis缓存与数据库的双写一致性(先更新数据库再删除缓存并结合RabbitMQ消息队列)

    实现双写一致性 通常会选择先更新数据库 然后再删除缓存的策略 并结合 RabbitMQ 的消息队列机制 主要有以下几个原因 保证数据一致性 在应用程序中 同一个数据可能存在于多个缓存服务节点中 这样会对数据的一致性造成很大影响 为了避免出现
  • JAVASE 注解与反射

    注解与反射都是框架的基础 注解 注解的格式 注释名 参数名 参数值 可以使用在 package class method field上 作为辅助信息 内置注解 Override 重写方法 会检测方法名称 Deprecated 表明该方法已过
  • 2013年春季学期最佳博客内容奖评选开始啦

    各位亲爱的俱乐部主席们 大家好啊 暑假来啦 CSDN高校俱乐部的福利也来啦 首先感谢你们为高校俱乐部的工作所付出的一切努力 从高校俱乐部的首页改版之后起 大家开始使用博客来记录自己俱乐部的工作内容 并且向俱乐部会员和主席们分享自己的学习生活
  • 魅族mx4服务器无响应,魅族MX4刷机失败解决方法

    2014 12 23 10 25 24 魅族MX4刷机失败解决方法 标签 魅族MX4 魅族MX4刷机教程 魅族MX4刷机失败 魅族MX4ROM 对于很多魅族 MX4的新手 对刷机不太了解 很容易导致刷机失败 那么魅族 MX4刷机失败应该如何
  • xshell连接时显示“服务器发送了一个意外的数据包。received:3,expected:20“问题的解决方法

    最近安装了openbsd6 7版本 安装完后通过xshell连接 弹出一个错误对话框 提示 服务器发送了一个意外的数据包 received 3 expected 20 的错误信息 检查sshd服务是正常开启的 防火墙也没阻止 奇怪了 我又重
  • pandas ExcelWriter定制格式(定制表头、渲染颜色等,非ExcelWriter标准的创建模式)...

    2019独角兽企业重金招聘Python工程师标准 gt gt gt ExcelWriter这个插件有个坑 就是已经设置好的格式是无法更改 原因详见这里 的 而且有如下问题 1 无法分别给一个单元格写入值和样式 对于单个单元格 必须在写入值的
  • 逆序数组(递归和非递归)(详细)

    逆序数组 递归和非递归 一 非递归 二 递归 一 非递归 思路 将第一个元素和最后一个元素交换 再将第二个元素和倒数第二个元素交换 直到所有元素全部交换 假设有一个数组arr abcdef 我们令它的第一个元素为arr left 最后一个元
  • 个人网页制作 大学生个人网页设计 个人网站模板 简单静态HTML个人网页作品 HTML+CSS+JavaScript

    HTML5期末大作业 个人网站设计 明星汉良 7页 带轮播特效 HTML CSS JavaScript 学生DW网页设计作业成品 web课程设计网页规划与设计 web学生网页设计作业源码 常见网页设计作业题材有 个人 美食 公司 学校 旅游
  • 从软件工程的角度写机器学习5——SVM(支持向量机)实现

    SVM实现 SVM在浅层学习时代是主流监督学习算法 在深度学习时代也往往作为最后一个预测层使用 说深度学习击败了SVM的纯属扯淡 SVM算法总体流程 本系列文章旨在讲解机器学习算法的工程实现方法 不在于推导数学原理 因此想深入了解原理的请移