把要分类的对象(例如)一个特征向量与训练集中已知类标记的所有对象进行对比,并由k近邻对指派到哪个类进行投票。
比如说k=3,我们要找到下图中绿色原点属于哪个类别。k个最近的邻居哪个最多,就将该对象分给该类别。黑圈内是绿点的最近的3个邻居。
1.距离度量
KNN算法的核心在于要找到实例点的距离。要找邻居就要度量相似性。估量不同样本之间的相似性,通常采用的方法就是计算样本间的“距离”,相似性度量方法有:欧式距离、余弦夹角、曼哈顿距离、切比雪夫距离等。
2.k值选择
若k值较小,只有与输入实例较近(相似)的训练实例才会对预测结果起作用,预测结果会对近邻实例点非常敏感。如果近邻实例点恰巧是噪声,预测就会出错。容易发生过拟合。
若k较大,与输入实例较远的(不相似的)训练实例也会对预测起作用,容易使预测出错。k值的增大就意味着整体的模型变简单。
3.分类决策规则
分类决策规则是指找到k个邻居后该如何分类待分类对象。常用的方法有:投票表决(少数服从多数),加权投票法(根据距离的远近,对k个近邻的投票进行加权,距离越近则权重越大)
(1)计算当前待分类对象与训练集中已知类标记的所有对象的距离;
(2)按照距离递增次序排序;
(3)选取与待分类对象距离最小的k个训练实例;
(4)统计这k个实例所属各个类别数;
(5)将统计的类别数最多的类别作为待分类的预测类别
稠密SIFT省去了传统SIFT特征中尺度变换和采样点的步骤,直接在指定尺寸的采样窗口中对图像进行均匀采样。稠密SIFT特征提取方法不需要进行采样点筛选和特征归一化等繁琐计算,特征提取效率较高,易于实现。此外,通过均匀采样提取到的稠密特征能够更为全面地描述图像不同区域的差异信息,并且一定程度上兼顾到图像空间位置关系等全局信息,更适合图像表示和图像分类。
然而,尽管稠密SIF特征有如上优势,但依然无法替代稀疏SIFT,其中一个原因是图像的特征描述很大程度上依赖于图像的尺度,很多细节结构只存在于一定的尺度范围内,传统的稀疏SIFT通过高斯金字塔空间来实现图像的尺度变换,从而可以捕捉到原始图像中难以发现先的深层次细节信息。相比之下,稠密SIFT缺乏多尺度结构,只能发现先图像在单一尺度下表现出来的表层特征,不利于挖掘图像隐藏在深层次中的细节信息。
在这个实验中用欧氏距离进行度量,分类决策规则采用投票决策。
class KnnClassifier(object):
def __init__(self,labels,samples):
""" Initialize classifier with training data. """
#使用训练数据初始化分类器
self.labels = labels
self.samples = samples
def classify(self,point,k=3):
""" Classify a point against k nearest
in the training data, return label. """
#在训练数据上采用k近邻分类,并返回标记
# compute distance to all training points
#计算所有训练数据点的距离
dist = array([L2dist(point,s) for s in self.samples])
# sort them
#对他们进行排序
ndx = dist.argsort()
# use dictionary to store the k nearest
#用字典存储k近邻
votes = {}
for i in range(k):
label = self.labels[ndx[i]]
votes.setdefault(label,0)
votes[label] += 1
return max(votes, key=lambda x: votes.get(x))
def L2dist(p1,p2):
return sqrt( sum( (p1-p2)**2) )
先建立一些简单的而为示例数据集来说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用PIckle模块来保存创建的数据。
# -*- coding: utf-8 -*-
from numpy.random import randn
import pickle
from pylab import *
# create sample data of 2D points
n = 200
# two normal distributions
class_1 = 0.6 * randn(n,2)
class_2 = 1.2 * randn(n,2) + array([5,1])
labels = hstack((ones(n),-ones(n)))
# save with Pickle
#with open('points_normal.pkl', 'w') as f:
with open('points_normal_test.pkl', 'wb') as f:
pickle.dump(class_1,f)
pickle.dump(class_2,f)
pickle.dump(labels,f)
# normal distribution and ring around it
print ("save OK!")
class_1 = 0.6 * randn(n,2)
r = 0.8 * randn(n,1) + 5
angle = 2*pi * randn(n,1)
class_2 = hstack((r*cos(angle),r*sin(angle)))
labels = hstack((ones(n),-ones(n)))
# save with Pickle
#with open('points_ring.pkl', 'w') as f:
with open('points_ring_test.pkl', 'wb') as f:
pickle.dump(class_1,f)
pickle.dump(class_2,f)
pickle.dump(labels,f)
print ("save OK!")
运行上面的代码,我们将会得到4个二维数据及文件,每个分布都有两个文件,我们可以将一个用来训练,另一个用来做测试。
# -*- coding: utf-8 -*-
import pickle
from pylab import *
from PCV.classifiers import knn
from PCV.tools import imtools
pklist=['points_normal.pkl','points_ring.pkl']
figure()
# 利用Pickle导入二维数据集
for i, pklfile in enumerate(pklist):
with open(pklfile, 'rb') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# load test data using Pickle
#用Pickle模块载入测试数据
with open(pklfile[:-4]+'_test.pkl', 'rb') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
model = knn.KnnClassifier(labels,vstack((class_1,class_2)))
# test on the first point
#在测试数据集的第一个数据点上进行测试
print (model.classify(class_1[0]))
#define function for plotting
#定义绘图函数
def classify(x,y,model=model):
return array([model.classify([xx,yy]) for (xx,yy) in zip(x,y)])
# lot the classification boundary
#绘制分类边界
subplot(1,2,i+1)
imtools.plot_2D_boundary([-6,6,-6,6],[class_1,class_2],classify,[1,-1])
titlename=pklfile[:-4]
title(titlename)
show()
在上面的代码中我们创建了一个简短的辅助函数以获取x和y二维坐标数组和分类器,并返回一个预测的类标记数组。现在把函数作为参数传递给实际的绘图函数,该绘图函数我们将其添加到imtools中:
def plot_2D_boundary(plot_range,points,decisionfcn,labels,values=[0]):
""" Plot_range is (xmin,xmax,ymin,ymax), points is a list
of class points, decisionfcn is a funtion to evaluate,
labels is a list of labels that decisionfcn returns for each class,
values is a list of decision contours to show. """
#Plot_range为(xmin,xmax,ymin,ymax),points是类数据点列表,decisionfcn是评估函数。
#Labels是函数decidionfcn关于每个类返回的标记列表。
#不同的类用不同颜色标记
clist = ['b','r','g','k','m','y'] # colors for the classes
# evaluate on a grid and plot contour of decision function
#在一个网格上进行评估,并画出决策函数的边界
x = arange(plot_range[0],plot_range[1],.1)
y = arange(plot_range[2],plot_range[3],.1)
xx,yy = meshgrid(x,y)
xxx,yyy = xx.flatten(),yy.flatten() # lists of x,y in grid 网格中的x,y坐标点列表
zz = array(decisionfcn(xxx,yyy))
zz = zz.reshape(xx.shape)
# plot contour(s) at values
#以values画出边界
contour(xx,yy,zz,values)
# for each class, plot the points with '*' for correct, 'o' for incorrect
#对于每类,用*画出分类正确的点,用o画出分类不正确的点
for i in range(len(points)):
d = decisionfcn(points[i][:,0],points[i][:,1])
correct_ndx = labels[i]==d
incorrect_ndx = labels[i]!=d
plot(points[i][correct_ndx,0],points[i][correct_ndx,1],'*',color=clist[i])
plot(points[i][incorrect_ndx,0],points[i][incorrect_ndx,1],'o',color=clist[i])
axis('equal')
这个函数需要一个决策函数,并且用meshgrid()函数在一个网格上进行预测。预测函数的等值线可以显示边界的位置,默认边界为零等值线。
随机数的生成使用的是python的numpy.random的randn函数,此函数生成标准正太分布的随机数。以上述代码中的”0.8 * randn(n,1) + 5“的意思就是方差为0.8,均值为5的正太分布数据。
画出的结果如下所示:
左图的class_1的方差是0.6,class_1的方差是1.2,他们俩的数据的中心点就分开来了,而之所以class_2的数据画出来感觉比较大是因为class_1的数据比较集中,而class_2=“1.2 * randn(n,2) + array([5,1])”,意思就是class_2的数据x轴跟y轴的正太分布的方差都是1.2,但是x轴的方差是5,y轴的方差是1。方差影响的是数据的离散程度,值越大越分散,而在正太分布中曲线是越扁平,所以大家可以看到class_2的数据分布类似椭圆形,而class_1的数据分布类似圆形(因为x轴和y轴的方差一样都是0)
右图的class_1 = 0.6 * randn(n,2),理解如上。而class_2的设置如下
r = 0.8 * randn(n,1) + 5
angle = 2*pi * randn(n,1)
class_2 = hstack((r*cos(angle),r*sin(angle)))
也就是我们先用randn函数正太分布出圆形的半径r,以及随机的角度,根据得到的半径及角度可算出其对应的坐标。所以也就是说如果我们想要自己测试看的话,为了能够让其能跟class_1的数据集分开,就要保证均值要够大才行,方差要跟class_1的相近。
在右图中,
们可以通过修改class_1及class_2的随机函数来修改数据集。
#左图的数据集
class_1 = 0.6 * randn(n,2)+0.6
class_2 = 1.2 * randn(n,2) + array([5,1])
#右图的数据集
class_1 = 0.6 * randn(n,2)
r = 0.8 * randn(n,1) + 2
angle = 2*pi * randn(n,1)
class_2 = hstack((r*cos(angle),r*sin(angle)))
labels = hstack((ones(n),-ones(n)))
下图是修改class_1和class_2的结果图
跟上图的数据集相比,我将左图的class_1的均值加0.6,说实话单个看的话感觉没变多少,因为我x轴跟y轴的数据的均值都是0.6,所以相比之前其数据会稍微分散了一些。
将右图的class_2的半径r的均值的值从6改为2。红色数据(class_2)会比较集中,是因为均值比较小,数据比较集中。
k值选择
若k值较小,只有与输入实例较近(相似)的训练实例才会对预测结果起作用,预测结果会对近邻实例点非常敏感。如果近邻实例点恰巧是噪声,预测就会出错。容易发生过拟合。
若k较大,与输入实例较远的(不相似的)训练实例也会对预测起作用,容易使预测出错。k值的增大就意味着整体的模型变简单。
因为我们自己拍摄手势的照片,我们希望手势的形状可以都提取到其特征,而用稀疏SIFT,可能不会把我们要的特征都提取出来,可能提取到的特征比较多的是背景,这与我们的需求不符,为了能够保证手势的形状特征都能够提取出来,我们使用稠密SIFT描述子来表示这些手势图像。下面的代码功能是展现生成的带有稠密SIFT子的图像。
# -*- coding: utf-8 -*-
import os
from PCV.localdescriptors import sift, dsift
from pylab import *
from PIL import Image
imlist=['gesture/train/C-uniform02.ppm','gesture/train/B-uniform01.ppm',
'gesture/train/A-uniform01.ppm','gesture/train/Five-uniform01.ppm',
'gesture/train/Point-uniform01.ppm','gesture/train/V-uniform01.ppm']
figure()
for i, im in enumerate(imlist):
print (im)
dsift.process_image_dsift(im,im[:-3]+'dsift',11,5,True)
l,d = sift.read_features_from_file(im[:-3]+'dsift')
dirpath, filename=os.path.split(im)
im = array(Image.open(im))
#显示手势含义title
titlename=filename[:-14]
subplot(2,3,i+1)
sift.plot_features(im,l,True)
title(titlename)
show()
import os
from PCV.localdescriptors import sift, dsift
from PCV.tools import imtools
from pylab import *
from PIL import Image
imlist = imtools.get_imlist('gesture/train')
for filename in imlist:
featfile = filename[:-3]+'dsift'
dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50));
这里将图像分辨率调成了固定的大小。如果不调整的话,可能会导致后面这些图像生成的描述子数量不一,从而每幅图像的特征向量长度不一样,会导致在后面比较它们时出错。
正式的手势识别实验
代码
# -*- coding: utf-8 -*-
from PCV.localdescriptors import dsift
import os
from PCV.localdescriptors import sift
from pylab import *
from PCV.classifiers import knn
def get_imagelist(path):
""" Returns a list of filenames for
all jpg images in a directory. """
return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]
def read_gesture_features_labels(path):
# 多所有以.dsift为后缀的文件创建一个列表
featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
# 读取特征
features = []
for featfile in featlist:
l,d = sift.read_features_from_file(featfile)
features.append(d.flatten())
features = array(features)
# 创建标记
labels = [featfile.split('/')[-1][0] for featfile in featlist]
return features,array(labels)
def print_confusion(res,labels,classnames):
n = len(classnames)
# 混淆矩阵
class_ind = dict([(classnames[i],i) for i in range(n)])
confuse = zeros((n,n))
for i in range(len(test_labels)):
confuse[class_ind[res[i]],class_ind[test_labels[i]]] += 1
print ('Confusion matrix for')
print (classnames)
print (confuse)
filelist_train = get_imagelist('gesture/train')
filelist_test = get_imagelist('gesture/test')
imlist=filelist_train+filelist_test
# process images at fixed size (50,50)
for filename in imlist:
featfile = filename[:-3]+'dsift'
dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
features,labels = read_gesture_features_labels('gesture/train/')
test_features,test_labels = read_gesture_features_labels('gesture/test/')
classnames = unique(labels)
# 测试 kNN
k = 1
knn_classifier = knn.KnnClassifier(labels,features)
res = array([knn_classifier.classify(test_features[i],k) for i in
range(len(test_labels))])
# 准确率
acc = sum(1.0*(res==test_labels)) / len(test_labels)
print ('Accuracy:', acc)
print_confusion(res,test_labels,classnames)
实验结果
k值对分类的影响
1.该结果是在k=1,以及稠密SIFT图像描述子的大小为10,位置之间的步长为5的情况下的结果,正确率为94.12%,Four的有张手势照片出错。
左边是“A”,右边是“Four”。
2.下图结果是在k=3,以及稠密SIFT图像描述子的大小为10,位置之间的步长为5的情况下的结果,正确率为88.23%。
3.下图结果是在k=6,以及稠密SIFT图像描述子的大小为10,位置之间的步长为5的情况下的结果,正确率为76.47%。
结果表明:k较大,与输入实例较远的(不相似的)训练实例也会对预测起作用,容易使预测出错。
稠密SIFT图像描述子的大小对分类的影响
1.下图结果是在稠密SIFT图像描述子的大小为10,位置之间的步长为5,以及k=1的情况下的结果,正确率为94.12%,
2.下图结果是在稠密SIFT图像描述子的大小为5,位置之间的步长为5,以及k=1的情况下的结果,正确率为94.12%,
3.下图结果是在稠密SIFT图像描述子的大小为2,位置之间的步长为5,以及k=1的情况下的结果,正确率为94.12%,
4.下图结果是在稠密SIFT图像描述子的大小为15,位置之间的步长为5,以及k=1的情况下的结果,正确率为94.12%,
5.下图结果是在稠密SIFT图像描述子的大小为25,位置之间的步长为5,以及k=1的情况下的结果,正确率为94.12%,
1) 1,2,3的实验是描述子的大小逐渐减小,正确率没有改变。4到5是描述子的增大,正确率没有改变。
5.下图结果是在稠密SIFT图像描述子的大小为25,位置之间的步长为5,以及k=6的情况下的结果,正确率为82.36%.我们在前面的实验曾经测试过在“k=6,以及稠密SIFT图像描述子的大小为10,位置之间的步长为5的情况下的结果,正确率为76.47%”。
6.下图结果是在稠密SIFT图像描述子的大小为5,位置之间的步长为5,以及k=6的情况下的结果,正确率为82.36%.我们在前面的实验曾经测试过在“k=6,以及稠密SIFT图像描述子的大小为10,位置之间的步长为5的情况下的结果,正确率为76.47%”。
2) 在相同步长的情况下,描述子小的话,每个特征所包含的信息会少,相反大的话,每个特征所包含的信息多。但在实验中我们需要的最好的描述子的大小,是能够确保每个描述子能够将我们所需要的信息都包含进去。综上实验表明,特征描述子的大小在分类器的k值非常适合时,可能不会再影响到实验的结果,不会让实验结果变得更好,也不会更糟糕,但在k值不是那么合适的时候,特征描述子的大小会影响到实验结果,从上面的5跟6可以看出特征描述子不管变小还是变大,在k值不合适的时候都会增加分类的正确性。