使用核技巧的线性分类器

. 线性分类:核方法

  • 1.数据封装:记得以前读weka源码的时候,它将样本封装到一个叫Instance的对象里面,整个数据集叫Instances里面存放的是单个样本instance,封装的好处是方便后期对样本的处理,这里将每一个样本封装为一个对象包含datatarget,记做Data
class Data:
        def __init__(self,row):
            self.data = map(float,row[:-1])
            self.target = int(row[-1])

将整个数据集放入一个列表中

rows = []
rows.append(Data(line.split(","))
  • 2.基本的线性分类器:原理是寻找每个类别所有数据的平均值,得到一个代表该类别的中心点,有新数据要对其进行分类时,只需要通过判断距离哪个中心点位置近进行分类。
def train(rows):
        average = {}#用来存放不同类别的中心点
        counts = {}
        for row in rows:
            c1 = row.target
            average.setdefault(c1,[0.0]*len(row.data))#定义一个长度与特征维度相等的list,
            counts.setdefault(c1,0)
            for i in range(len(row.data)):
                average[c1][i] += float(row.data[i])
            counts[c1] += 1
            
        for c,avg in average.items():
            for i in range(len(avg)):
                avg[i] /= counts[c]#average的值会修改
        return average

思想很简单,将每一类别样本的值相加,再除以样本的个数。有一点值得注意,average是一个字典,key为类别,value为一个list,在修改value的时候,average的值也跟着变化

  • 3.距离的计算
    在对测试样本进行分类的时候需要计算样本与类别中心点的距离,可以使用欧氏距离,如图:就是计算x到中心点c1、c2哪个更近。不过我们也可以使用向量的夹角,因为向量是矢量,向量的乘积是有符号的,通过判断结果的 正、负来找出距离哪个中心点近。

    class = sign((X - (C1+C2)/2) * (C2 - C1)) ==>sign(xC1-XC2 + (C1C1 - C2C2)/2)
    通过计算向量夹角判断样本的类别
  • 4.特征的处理:针对每一特征维度,定制不同的特征处理方法,最后再形成新的数据集
    • 固定个数标称型特征,如Yes or No,可以化为1,0类型
    • 个数不固定的标称型特征,如足球,篮球,滑雪,看书等等,可以对特征进行按层级排列,例如篮球和足球都属于球类,球类都属于运动,在转化为数值特征的时候就不再是1了,例如:足球;0.8滑雪:0.6
    • 在处理位置特征时候,可以借助地图API计算距离
      最后的合并成新的数据集[f1(row[0]),f2(row[1]),f3(row[2])],其中f1、f2、f3是特征处理方法,row[0]、row[1]是特征项
  • 4.对特征进行缩放:(加黑了,说明很重要),在处理特征的时候,特征的尺度不一样,比如年龄的范围是0100,薪资的范围为1000100000,如果直接用作分类器时效果可能会很差,所以我们需要对其归一化,对原始数据进行线性变换,使结果映射到[0-1]之间,通过找出特征的最大值和最小值,将数据都缩放到同一尺度,公式如下:

    其中max为样本的最大值,min为样本的最小值,这个方法有一个缺陷当有新的数据加入时,可能导致max和min发生变化;具体的操作:
def scala(rows):   
        #[(min,max),(),()]
        ranges = [(min([row.data[i] for row in rows]),max([row.data[i] for row in rows]))
                for i in range(len(rows[0].data))]
        #(x - min) / (max - min)
        scalaFun = lambda d:[(d[i] - ranges[i][0]) / (ranges[i][1] - ranges[i][0]) for i in range(len(ranges))]
        newrows = [Data(scalaFun (row.data) + [row.target]) for row in rows]
        return newrows, scalaFun 

返回两部分,一个是处理后的数据集,一个是scala函数,用于处理新样本。至于为什么写成匿名函数,这样就能够保存ranges

  • 5.核方法:设想,我们有一堆样本,将数据绘制在二维平面上,发现正负样本呈环状展示,这时候本文上面提到的方法就已经失效了,不能使用一条直线将样本分开,但如果我们将样本投影到一个3维的空间中,那么样本就将变得线性可分,如下图:

    这种方法叫做核技巧,初学者可能听到核方法就会想到SVM,是SVM引用了核方法,而不是它创造了核方法,下文就将使用核函数。核技巧是用一个新的函数替代原来的点积函数,借助某个映射函数将数据变换到更高维度的坐标空间,新函数返回新的内积结果。实际上我们不会去找这个映射函数,因为找到一个符合数据集的高纬函数是很困难的,常常我们使用被受人推崇的径向基函数(radial-basis function,rbf)
    rbf核
def rbf(v1,v2,gamma=20):
        m = sum([(v1[i] - v2[i])**2 for i in range(len(v1))])
        return math.exp(-gamma * m)

这时候我们将样本映射到新的空间中,我们需要一个新的函数,用以计算坐标点在变换后的空间中与均值点的距离,在新的样本空间中,无法计算均值点。所幸的是,先对一组向量求均值,然后计算均值与向量\(a\)的点积结果,与先对向量\(a\)与该组向量中的每一个向量求点积,然后再计算均值是完全等价的。因此

for row in rows:
        if row.target == 0:
            sum0 += rbf(sample,row.data,gamma)#在计算内积的时候用核函数替代
            count0 += 1
        else:
            sum1 += rbf(sample,row.data,gamma)
            count1 += 1
    y = (1.0/count0) *sum0 - (1.0/count1) *sum1 + offset

完整代码见这里
总结:本节使用了线性分类器对数据进行二分类,对于不能线性分类的数据引入了核函数,认识了核函数的工作原理,有助于对SVM高级分类器的理解。

posted on 2016-11-23 23:12  Wanna_Go  阅读(1730)  评论(2编辑  收藏  举报

导航