这个算法真是不太好懂,看了好几遍终于有点入门的感觉,就赶紧记录下这点感觉。我从复习线性分类开始,然后复习点积的含义,再引出核方法。
线性分类是最容易理解的分类方法,两组数据A和B,分别求出A和B的平均值,比如M和N,当判断新数据X是属于A还是属于B呢,就看新数据X到M近还是N近,X属于距离近的那个。为了实现这个算法,我们需要计算出各分类的均值点:
def lineartrain(rows):
averages={}
counts={}
for row in rows:
# Get the class of this point
cl=row.match
averages.setdefault(cl,[0.0]*(len(row.data)))
counts.setdefault(cl,0)
# Add this point to the averages
for i in range(len(row.data)):
averages[cl][i]+=float(row.data[i])
# Keep track of how many points in each class
counts[cl]+=1
# Divide sums by counts to get the averages
for cl,avg in averages.items():
for i in range(len(avg)):
avg[i]/=counts[cl]
return averages
下来就可以给分类了,计算要分类的坐标点到各个分类的均值点的距离,然后从中选择距离最近者。
这里我们采用另一种方法,使用向量和点积。向量具有大小和方向,我们通常将其表示成平面的一个箭头。所谓点积,针对两个向量,将第一个向量中的每个值雨第二个向量中的对应值相乘,然后再将所得的每个乘积相加,最后得到一个总的结果。
def dotproduct(v1,v2):
return sum([v1[i]*v2[i] for i in range(len(v1))])
点积也可以利用两个向量的长度乘积,再乘以两者夹角的余弦求得。最重要的是,如果夹角的度数大于90度,夹角余弦值为负,这样点积结果也就是负了。
M0和M1分别是分类0和分类1的均值点,C为M0和M1的中心点,还有两个点即将分类的点X1和点X2。很明显X1离M0近一点,应该划归为分类0,X2离M1近一些,应该划归为分类1。同时也注意到,向量X1--->C的向量M0--->M1的夹角小于90度,因此X1--->C和M0--->M1的点积结果为正数;向量X2--->C的向量M0--->M1的夹角大于90度,因此X2--->C和M0--->M1的点积结果为负数。由此,只需通过观察点积结果的正负,就可以判断出新坐标的类属。
点C为M0和M1的均值点,亦即(M0+M1)/2,所以寻找分类的公式如下:
class=sign((X-(M0+M1)/2) . (M0-M1))
相乘后的结果为:
class=sign(X.M0 - X.M1 + (M1.M1 - M0.M0)/2)
我们可以利用上面的公式来确定分类了:
def dpclassify(point,avgs):
b=(dotproduct(avgs[1],avgs[1])-dotproduct(avgs[0],avgs[0]))/2
y=dotproduct(point,avgs[0])-dotproduct(point,avgs[1])+b
if y>0: return 0
else: return 1
到这里,一切都很正常。正常的事情往往又不正常,目前正常的局面是,可以用到两个点的距离,也就是一个超平面来分割开分类0和分类1。不正常的局面是,面对下图着这种情况好像又鞭长莫及了,分类的均值点重合,尽管大家很清楚,任何位于圈圈内的都是X,圈圈外的都是O,但是线性分类器却无法识别这两个分类。
可是我们又发现,如果对每个点的x和y求平方,会是什么样呢?这样所有X都偏移到了图上的角落处,所有的O位于角落以外的区域,这样我们就可以用线性分类器来处理了。
上面的两站图说明:通过对坐标点的变换,构造出一个只用一条直线进行划分新数据是有可能的。有时我们需要升级维度,比如将一个x和y坐标的数据集,变换成一个由a,b,c三个坐标构成的新数据集,其中a=x*x,b=x*y,c=y*y。
尽管我们可以把数据依照上面的方式变换到新的坐标系中,但通常我们不会这么去做,下面来聊聊核技法。核技法的思路是用一个新的函数取代上面的点积函数,而不是将数据变换奥新坐标系中。一种备受推崇的方法被成为径向基函数,这个函数于点积类似,接受两个向量作为输入参数,并返回一个标量,与点积不同的是,径向基函数是非线性的,因而它能够将数据映射到更为复杂的空间中。
def rbf(v1,v2,gamma=10):
dv=[v1[i]-v2[i] for i in range(len(v1))]
l=veclength(dv)
return math.e**(-gamma*l)
现在我们需要一个新函数,来计算坐标点在变换后的空间中与均值点的距离。遗憾的是,目前的均值是在原始空间中计算得到的,因为此处无法直接使用他们。所幸的是,先对一组向量求均值,然后再计算均值与向量A的点积结果,与先对向量A与该组向量中的每一个向量求点积,然后再计算均值,在效果上是等价的。因此,我们不再尝试对分类的两个坐标点求点积,也不在计算某个分类的均值点,取而代之的是,计算出某个坐标点与分类中其余每个坐标点之间的点积或径向基函数的结果,然后在对他们求均值。
再来看一遍这个函数
class=sign(X.M0 - X.M1 + (M1.M1 - M0.M0)/2)。
我们不直接去求M0和M1了,但心里必须清楚M0,M1是中心点,是径向基函数结果的平均值。
def nlclassify(point,rows,offset,gamma=10):
sum0=0.0
sum1=0.0
count0=0
count1=0
for row in rows:
if row.match==0:
sum0+=rbf(point,row.data,gamma)
count0+=1
else:
sum1+=rbf(point,row.data,gamma)
count1+=1
y=(1.0/count0)*sum0-(1.0/count1)*sum1+offset
if y>0: return 0
else: return 1
def getoffset(rows,gamma=10):
l0=[]
l1=[]
for row in rows:
if row.match==0: l0.append(row.data)
else: l1.append(row.data)
sum0=sum(sum([rbf(v1,v2,gamma) for v1 in l0]) for v2 in l0)
sum1=sum(sum([rbf(v1,v2,gamma) for v1 in l1]) for v2 in l1)
return ((1.0/(len(l1)**2))*sum1-(1.0/(len(l0)**2))*sum0)/2
注意函数getoffset函数就是求 (M1.M1 - M0.M1)/2,nlclassify函数就是具体的分类了。