. 线性分类:核方法
- 1.数据封装:记得以前读weka源码的时候,它将样本封装到一个叫
Instance
的对象里面,整个数据集叫Instances
里面存放的是单个样本instance,封装的好处是方便后期对样本的处理,这里将每一个样本封装为一个对象包含data
和target
,记做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高级分类器的理解。