目录
8.1 K临近分类法(KNN)
8.1.1 一个简单的二维示例
8.1.2 用稠密SIFT作为图像特征
8.1.3 图像分类:手势识别
8.2 贝叶斯分类器
用PCA降维
8.3 支持向量机
8.3.1 使用LibSVM
在分类算法中,最简单且用的最多的一种方法之一就是KNN(K-Nearset Neighbor,K邻近分类法),这种算法把要分类的对象(例如一个特征向量)与训练集中已知类标记的所有对象进行对比,并由k近邻对指派到哪个类进行投票。这种方法通常分类效果比较好,但是也有很多弊端:与K-means聚类算法一样,需要预先设定k值,k值的选择会影响分类的性能;此外,这种方法要求将整个训练集存储起来,如果训练集非常大,搜索起来就非常慢。对于大训练集,采取某些装箱形式通常会减少对比的次数从积极的一面来看,这种方法在采用何种距离度量方面是没有限制的;实际上,对于你所能想到的东西他都可以奏效,但这并不意味这对任何东西它的分类性能都很好。另外,这种算法的可并行性也很一般。
实现最基本的KNN形式非常简单。给定训练样本集和对应的标记列表,下面的代码可以用来完成这一工作。这些训练样本和标记可以在一个数组里成行摆放或者干脆摆放列表里,训练样本可能是数字、字符串等任何你喜欢的形状。将定义的类对象添加到名为knn.py的文件里:
from numpy import *
class KnnClassifier(object):
def __init__(self, labels, samples):
"""使用训练数据初始化分类器"""
self.labels = labels
self.samples = samples
def classify(self, point, k=3):
"""在训练数据上采用k近邻分类,并返回标记"""
# 计算所有训练数据点的距离
dist = array([sqrt(np.sum((s - point)**2)) for s in self.samples])
# 对它们进行排序
ndx = dist.argsort()
# 用字典存储k近邻
votes = {}
for i in range(k):
label = self.labels[ndx[i]]
votes.setdefault(label,0)
votes[label] += 1
return max(votes)
def L2dist(p1, p2):
return sqrt( sum((p1-p2)**2) )
定义一个类并用训练数据初始化非常简单;每次相对某些东西进行分类时,用KNN方法,我们就没有必要存储并将训练数据作为参数来传递。用一个字典来存储邻近标记,我们便可以用文本字符串或数字来表示标记。在这个例子中,我们用欧式距离 (L2) 进行度量,也可以使用其他度量方式,只需要将其作为函数添加到上面代码的最后。
我们首先建立一些简单的二维示例数据集来说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用Pickle模块来创建保存创建的数据:
from numpy.random import randn
import pickle
from pylab import *
# create sample data of 2D points
# 创建二维样本数据
n = 200
# two normal distributions
# 两个正态分布数据集
class_1 = 0.2 * randn(n, 2)
class_2 = 1.6 * randn(n, 2) + array([5, 1])
labels = hstack((ones(n), -ones(n)))
# save with Pickle
# 用 Pickle 模块保存
# with open('points_normal.pkl', 'w') as f:
with open('811/points_normal.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
# 用 Pickle 保存
# with open('points_ring.pkl', 'w') as f:
with open('811/points_ring.pkl', 'wb') as f:
pickle.dump(class_1, f)
pickle.dump(class_2, f)
pickle.dump(labels, f)
用不同的保存文件名运行该脚本两次,例如第一次用代码中的文件名进行保存,第二次将代码中的 points_normal.pkl 和 points_ring.pkl 分别改为 points_normal_test. pkl 和 points_ring_test.pkl 进行保存。你将得到4个二维数据集文件,每个分布都有两个文件,我们可以将一个用来训练,另一个用来做测试。
用下面的代码来创建一个脚本:
import pickle
from pylab import *
from PCV.classifiers import knn
from PCV.tools import imtools
# 用 Pickle 载入二维数据点
with open('811/points_normal.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)))
# 用Pickle模块载入测试数据
with open('811/points_normal_test.pkl', 'rb') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
#在测试数据集的第一个数据点上进行测试
print (model.classify(class_1[0]))
#为了可视化所有测试数据点的分类,并展示分类器将两个不同的类分开得怎样,我 们可以添加这些代码:
# 定义绘图函数
def classify(x,y,model=model):
return array([model.classify([xx,yy]) for (xx,yy) in zip(x,y)])
# 绘制分类边界
imtools.plot_2D_boundary([-6,6,-6,6],[class_1,class_2],classify,[1,-1])
show()
书上还给出了创建一个简短的辅助函数以获取x和y二维坐标数组和分类器,并返回一个预测的类标记数组,把函数作为参数传递给实际的绘图函数的方式。该函数已经预制在PCV包的 imtools 文件中。
def plot_2D_boundary(plot_range,points,decisionfcn,labels,values=[0]):
""" Plot_range 为(xmin,xmax,ymin,ymax), points 是类数据点列表,decisionfcn 是评估函数,labels 是函数 decidionfcn 关于每个类返回的标记列表 """
clist = ['b','r','g','k','m','y'] # 不同的类用不同的颜色标识
# 在一个网格上进行评估,并画出决策函数的边界
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() # 网格中的 x,y 坐标点列表
zz = array(decisionfcn(xxx,yyy))
zz = zz.reshape(xx.shape)
# plot contour(s) at values
contour(xx,yy,zz,values)
#对于每类,用 * 画出分类正确的点,用 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')
每个示例中,不同颜色代表类标记,正确分类的点用星号表示,分类错误的点用圆点表示,曲线是分类器的决策边界。正如所看到的,kNN 决策边界适用于没有任何明确模型的类分布。
要对图像进行分类,我们需要一个特征向量来表示一幅图像。在整幅图像上用一个规则的网格应用SIFT描述子可以得到稠密SIFT的表示形式。创建一个名为 dsift.py 的文件,并添加下面代码到该文件中:
from PIL import Image
import os
from numpy import *
from PCV.localdescriptors import sift
def process_image_dsift(imagename, resultname, size=20, steps=10, force_orientation=False, resize=None):
""" 用密集采样的 SIFT 描述子处理一幅图像,并将结果保存在一个文件中。可选的输入: 特征的大小 size,位置之间的步长 steps,是否强迫计算描述子的方位 force_orientation (False 表示所有的方位都是朝上的),用于调整图像大小的元组 """
im = Image.open(imagename).convert('L')
if resize != None:
im = im.resize(resize)
m, n = im.size
if imagename[-3:] != 'pgm':
# 创建一个 pgm 文件
im.save('tmp.pgm')
imagename = 'tmp.pgm'
# 创建帧,并保存到临时文件
scale = size / 3.0
x, y = meshgrid(range(steps, m, steps), range(steps, n, steps))
xx, yy = x.flatten(), y.flatten()
frame = array([xx, yy, scale * ones(xx.shape[0]), zeros(xx.shape[0])])
savetxt('tmp.frame', frame.T, fmt='%03.3f')
if force_orientation:
cmmd = str("sift " + imagename + " --output=" + resultname +
" --read-frames=tmp.frame --orientations")
else:
cmmd = str("sift " + imagename + " --output=" + resultname +
" --read-frames=tmp.frame")
os.system(cmmd)
print('processed', imagename, 'to', resultname)
为了使用命令行处理,用 savetxt() 函数将帧数组存储在一个文本文件中,该函数的最后一个参数可以在提取描述子之前对 图像的大小进行调整,例如,传递参数 imsize=(100, 100) 会将图像调整为 100×100 像素的方形图像。最后,如果 force_orientation 为真,则提取出来的描述子会基于局部主梯度方向进行归一化;否则,则所有的描述子的方向只是简单地朝上。
利用类似下面的代码可以计算稠密 SIFT 描述子,并可视化它们的位置:
# -*- coding: utf-8 -*-
import dsift
from pylab import *
from PIL import Image
from PCV.localdescriptors import sift
#特征大小50,步长30
dsift.process_image_dsift('path3/sky1.jpg','path3/sky1.sift',50,30,True)
l,d = sift.read_features_from_file('path3/sky1.sift')
im = array(Image.open('path3/sky1.jpg'))
sift.plot_features(im,l,True)
show()
选择了特征大小为50,步长30。反映到图像中即为圆圈大小和圆心间隔。
在网站https://www.idiap.ch/webarchives/sites/www.idiap.ch/resource/gestures/下载数据较小的测试集(16.3Mb),将下载后的所有图像放在一个名为uniform的文件夹里,每一类均分两组,并分别放入名为train和test的两个文件夹中。
可以通过下面的代码得到每幅图像的稠密 SIFT 特征:
import dsift
# 将图像尺寸调为 (50,50),然后进行处理
for filename in imlist:
featfile = filename[:-3]+'dsift'
dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
一个例子:
from PCV.localdescriptors import sift, dsift
from pylab import *
from PIL import Image
import os
from PIL import Image
imlist = ['uniform/train/C-uniform01.ppm', 'uniform/train/B-uniform01.ppm',
'uniform/train/A-uniform01.ppm', 'uniform/train/Five-uniform01.ppm',
'uniform/train/Point-uniform01.ppm', 'uniform/train/V-uniform01.ppm']
figure()
for i, im in enumerate(imlist):
print(im)
dsift.process_image_dsift(im, im[:-3] + 'dsift', 10, 5, True, resize=(50,50))
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()
定义一个辅助函数,用于从文件中读取稠密 SIFT 描述子,并得出准确度和混淆矩阵。代码如下:
from PCV.localdescriptors import sift, dsift
from pylab import *
from PIL import Image
import os
from PIL import Image
import PCV.classifiers.knn as knn
def read_gesture_features_labels(path):
# create list of all files ending in .dsift
featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
# read the features
features = []
for featfile in featlist:
l,d = sift.read_features_from_file(featfile)
features.append(d.flatten())
features = array(features)
# create labels
labels = [featfile.split('/')[-1][0] for featfile in featlist]
return features,array(labels)
def print_confusion(res,test_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)
def get_imlist(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('.ppm')]
imlist1 = get_imlist('uniform/train/')
imlist2 = get_imlist('uniform/test/')
# 将图像尺寸调为(50,50),然后进行处理
for filename in imlist1:
featfile = filename[:-3]+'dsift'
dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
for filename in imlist2:
featfile = filename[:-3]+'dsift'
dsift.process_image_dsift(filename,featfile,10,5,resize=(50,50))
features,labels = read_gesture_features_labels('uniform/train/')
test_features,test_labels = read_gesture_features_labels('uniform/test/')
classnames = unique(labels)
# test 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))])
# accuracy
acc = sum(1.0*(res==test_labels)) / len(test_labels)
print ('Accuracy:', acc)
print_confusion(res,test_labels,classnames)
另一个简单却有效的分类器是贝叶斯分类器(或称朴素贝叶斯分类器)。贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相关的(这就是它“朴素”的部分)。贝叶斯分类器可以非常有效地被训练出来,原因在于每一个特征模型都是独立选取的。尽管它们的假设非常简单,但是贝叶斯分类器已经在实际应用中获得显著成效,尤其是对垃圾邮件的过滤。贝叶斯分类器的另一个好处是,一旦学习了这个模型,就没有必要存储训练数据了,只需存储模型的参数。
该分类器是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最高的那个类构造出来的。
首先让我们看一个使用高斯概率分布模型的贝叶斯分类器基本实现,也就是用从训练数据集计算得到的特征均值和方差来对每个特征单独建模。把下面的BayesClassifier类添加到文件bayes.py中:
from pylab import *
class BayesClassifier(object):
def __init__(self):
"""使用训练数据初始化分类器"""
self.labels = [] # 类标签
self.mean = [] # 类均值
self.var = [] # 类方差
self.n = 0 # 类别数
def train(self,data,labels=None):
"""在数据data(n×dim的数组列表)上训练,标记labels是可选的,默认为0...n-1"""
if labels == None:
labels = range(len(data))
self.labels = labels
self.n = len(labels)
for c in data:
self.mean.append(mean(c,axis=0))
self.var.append(var(c,axis=0))
def classify(self,points):
"""通过计算得出的每一类的概率对数据点进行分类,并返回最可能的标记"""
# 计算每一类的概率
est_prob = array([gauss(m,v,points) for m,v in zip(self.mean, self.var)])
# 获取具有最高概率的索引,该索引会给出类标签
ndx = est_prob.argmax(axis=0)
est_labels = array([self.labels[n] for n in ndx])
return est_labels, est_prob
该模型每一类都有两个变量,即类均值和协方差。train()方法获得特征数组列表(每个类对应一个特征数组),并计算每个特征数组的均值和协方差。classify()方法计算数据点构成的数组的类概率,并选概率最高的那个类,最终返回预测的类标记及概率值,同时需要一个高斯辅助函数:
def gauss(m,v,x):
"""用独立均值m和方差v评估d维高斯分布"""
if len(x.shape) == 1:
n,d = 1, x.shape[0]
else:
n,d = x.shape
# 协方差矩阵,减去均值
S = diag(1/v)
x = x - m
# 概率的乘积
y = exp(-0.5 * diag(dot(x, dot(S, x.T))))
# 归一化并返回
return y * (2 * pi) ** (-d/2.0) / (sqrt(prod(v)) + 1e-6)
该函数用来计算单个高斯分布的乘积,返回给定一组模型参数 m 和 v 的概率。
将该贝叶斯分类器用于上一节的二维数据,下面的脚本将载入上一节的二维数据,并训练出一个分类器:
import pickle
import bayes
from PCV.tools import imtools
from pylab import *
# 用Pickle模块载入二维样本点
with open('811/points_normal.pkl','rb+') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 训练贝叶斯分类器
bc = bayes.BayesClassifier()
bc.train([class_1, class_2],[1,-1])
# 用Pickle模块载入测试数据
with open('811/points_normal_test.pkl','rb+') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 在某些数据点上进行测试
print(bc.classify(class_1[:10])[0])
# 绘制这些二维数据点及决策边界
def classify(x, y, bc=bc):
points = vstack((x, y))
return bc.classify(points.T)[0]
imtools.plot_2D_boundary([-6, 6, -6, 6], [class_1, class_2], classify, [1, -1])
show()
该脚本会将前10个二维数据点的分类结果打印输出到控制台,输出结果如下:
图中不同的颜色代表了类标记,星号代表正确分类,圆点代表错误分类,曲线是分类器的决策边界。
由于稠密SIFT描述子的特征向量十分庞大(从前的例子可以看到,参数的选取超过了10000),在用数据拟合模型之前进行降维处理是一个很好的想法。主成分分析法,非常适合用于降维。下面的脚本就是用PCA进行降维:
import pca
from pylab import *
from PCV.localdescriptors import sift, dsift
import bayes
def read_gesture_features_labels(path):
# create list of all files ending in .dsift
featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
# read the features
features = []
for featfile in featlist:
l,d = sift.read_features_from_file(featfile)
features.append(d.flatten())
features = array(features)
# create labels
labels = [featfile.split('/')[-1][0] for featfile in featlist]
return features,array(labels)
def print_confusion(res,test_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)
features, labels = read_gesture_features_labels('uniform/train/')
test_features, test_labels = read_gesture_features_labels('uniform/test/')
classnames = unique(labels)
# print(features,labels,classnames)
V, S, m = pca.pca(features)
# 保持最重要的成分
V = V[:50]
features = array([dot(V, f - m) for f in features])
test_features = array([dot(V, f - m) for f in test_features])
# 测试贝叶斯分类器
bc = bayes.BayesClassifier()
blist = [features[where(labels == c)[0]] for c in classnames]
# print(blist)
bc.train(blist, classnames)
res = bc.classify(test_features)[0]
acc = sum(1.0 * (res == test_labels)) / len(test_labels)
print('Accuracy:', acc)
print_confusion(res, test_labels, classnames)
虽然分类效果不如 K 临近分类器,但是贝叶斯分类器不需要保存任何训练数据,而且只需保存每个类的模型参数。这一结果会随着PCA维度选取的不同而发生巨大的变化。
SVM(Support Vector Machine,支持向量机)是一类强大的分类器,可以在很多分类问题中给出现有水准很高的分类结果。最简单的 SVM 通过在高维空间中寻找一个最优线性分类面,尽可能地将两类数据分开。对于一特征向量 x 的决策函数为:
其中w 是常规的超平面,b 是偏移量常数。该函数月阈值为 0,它能够很好地将两 类数据分开,使其一类为正数,另一类为负数。通过在训练集上求解那些带有标记的特征向量xi的最优化问题,使超平面在两类间具有最大分开间隔,从而找到上面决策函数中的参数w 和 b。该决策函数的常规解是训练集上某些特征向量的线性组合:
所以决策函数可以写为:
这里的 i 是从训练集中选出的部分样本,这里选择的样本称为支持向量,因为它们可以帮助定义分类的边界。
SVM的一个优势是可以使用核函数(kernel function);核函数能够将特征向量映射到另一个不同维度的空间中,比如高纬度空间。通过核函数映射,依然可以保持对决策函数的控制,从而可以有效地解决非线性问题或者很难的分类问题。用核函数K(xi,x)替代上面决策函数中的内积xi·x。
下面是一些常见的核函数:
每个核函数的参数都是在训练阶段确定的。
LibSVM是最好的、使用最广泛的SVM实现工具包。下载网址:https://www.csie.ntu.edu.tw/~cjlin/libsvm/
下面的脚本会载入在前面kNN范例分类中用到的数据点,并用径向基函数训练一个SVM分类器:
import pickle
from libsvm.svm import *
from libsvm.svmutil import *
with open('811/points_normal.pkl', 'rb') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 转换成列表,便于使用 libSVM
class_1 = list(map(list, class_1))
class_2 = list(map(list, class_2))
labels = list(labels)
samples = class_1 + class_2 # 连接两个列表
# 创建 SVM
prob = svm_problem(labels, samples)
param = svm_parameter('-t 2')
# 在数据上训练
m = svm_train(prob, param)
# 在训练数据上分类效果如何?
res = svm_predict(labels, samples, m)
按照书中代码,map(list,class_1)可能会报错。原因是在python2中map函数返回的是list数据类型,而在python3中返回的是map数据类型,该数据类型不能直接相加。解决方法如代码中所示,将其转换为list即可。
实验结果可知,400个数据点有399个分类正确。
载入其他数据集并进行测试:
# 用 Pickle 模块载入测试数据
with open('811/points_normal_test.pkl', 'rb') as f:
class_1 = pickle.load(f)
class_2 = pickle.load(f)
labels = pickle.load(f)
# 转换成列表,便于使用 LibSVM
class_1 = list(map(list,class_1))
class_2 = list(map(list,class_2))
# 定义绘图函数
def predict(x,y,model=m):
return array(svm_predict([0]*len(x),zip(x,y),model)[0])
# 绘制分类边界
imtools.plot_2D_boundary([-6,6,-6,6],[array(class_1),array(class_2)],predict,[-1,1])