文章目录
引言
本章介绍图像分类和图像内容分类算法,首先介绍一些简单有效的方法及性能最好的分类器,运用它们解决两类和多类分类问题,并展示两个用于手势识别和目标识别的运用实例。
8.1 K邻近分类法
全称为KNN(K-Nearest Neighbor),这种算法把要分类的对象与训练集中已标记的所有对象进行对比,由KNN指派到哪个类别,k值的选择会影响分类的性能,且要求把整个训练集存储起来,存储量会影响搜索的速度。对于大的训练集,采取装箱形式通常会减少对比的次数,在采取距离度量方面是没有限制的,但不是对所有东西的分类性能都很好。
实现KNN的过程要给定训练样本集和对应的标记列表,将定义的类对象添加到knn.py的文件里。
import math
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([L2dist(point,s) for s in self.samples])
# 从小到大排序后输入下标的数组
ndx = dist.argsort()
#字典形式存储k近邻
votes = {}
#三组
for i in range(k):
#存储为列表
labels = self.labels[ndx[i]]
#令label为查找的键值
votes.setdefault(label,0)
votes[label]+=1
return max(votes)
def L2dist(p1,p2):
return math.sqrt(sum((p1-p2)**2))
用字典来存储临近标记,可以用文本字符串或数字来表示标记,用欧式距离(L2)度量点与sample里的距离。
8.1.1 一个简单的二维示例
首先建立二维示例数据集说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用Pickle模块来保存创建的数据:
from numpy.random import randn
import pickle
n = 200
#两个正态分布数据集
class_1 = 0.6*randn(n,2)
class_2 = 1.2*randn(n,2)+array([5,1])
labels = hstack((ones(n),-ones(n)))
#用Pickle模块保存
with open('points_normal.pkl','wb') as f:
pickle.dump(class_1,f)
pickle.dump(class_2,f)
pickle.dump(labels,f)
# 正态分布,并使数据成环绕状分布
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)))
with open('points_ring.pkl','wb') as f:
pickle.dump(class_1,f)
pickle.dump(class_2,f)
pickle.dump(labels,f)
可以改变文件名,使用一个用来训练,另一个用来测试。
用Knn分类器来完成数据集的分类:
import Knn
import pickle
import imtools
with open('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)))
with open('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]))
>1.0
可视化数据点:
import imtools
# 定义绘图函数
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])
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)
# 以 values 画出边界
contour(xx,yy,zz,values)
# 对于每类,用 * 画出分类正确的点,用 o 画出分类不正确的点
for i in range(len(points)):
d = decisionfcn(points[i][:,0],points[i][:,1])
# labels是[1,-1],比较classify的结果
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')
normal
ring
这个函数需要一个决策函数(分类器),并且用 meshgrid() 函数在一个网格上进行预测。决策函数的等值线可以显示边界的位置,默认边界为零等值线。
8.1.2 用稠密SIFT作为图像特征
本节中,我们使用稠密SIFT作为图像的特征向量,对图像进行分类。利用规则的网格应用SIFT描述子可得到稠密的SIFT特征。
from PIL import Image
import os
from numpy import *
import sift
def process_image_dsift(imagename,resultname,size=20,steps=10,force_orientation=False,resize=None):
'''size是特征的大小,位置之间的步长是step,描述子方位(False表示所有的方位朝上,resize是调整图像大小的元组)'''
im = Image.open(imagename).convert('L')
if resize!=None:
im = im.resize(resize)
m,n = im.size
#图像名称后三位
if imagename[-3:] != 'pgm':
#create a pgm file
im.save('tmp.pgm')
imagename = 'tmp.pgm'
# create frames and save to temporary file
scale = size/3.0
#制作稠密的网格,用linspace也可以
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)
(fmt是什么?)
savetext()的最后一个参数可以在提取描述子之前对图像的大小进行调整;若force_orientation为真,描述子会基于局部主梯度方向进行归一化。
import dsift,sift
dsift.process_image_dsift('pic/empire.jpg','empire.sift',90,40,True)
l,d = sift.read_features_from_file('empire.sift')
im = array(Image.open('pic/empire.jpg'))
sift.plot_features(im,l,True)
如图所示,在整个图像中计算出稠密 SIFT 特征。
8.1.3 图像分类:手势识别
在此应用中,我们会用稠密SIFT描述子来表示这些手势图像,建立简单的手势识别系统,用静态手势和数据库的一些图像进行演示。
import dsift,imtools
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))
上面的代码对每一副图像创建特征文件,后缀是.dsift,图像分辨率要改成常见的固定大小,因为每幅图像的描述子数量不同,特征向量长度不一样,从而在比较时出错。
定义辅助函数,从文件中读取稠密的SIFT描述子:
import os, sift
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)
labels将每个featlist中文件名的首字母作为标记,如A,B,C,F(Five)等。第一个[-1]与[0]效果一致,要把字符串看作一个整体。
imlist = ['gesture/fig8-3/C-uniform01.ppm', 'gesture/fig8-3/B-uniform01.ppm',
'gesture/fig8-3/A-uniform01.ppm', 'gesture/fig8-3/Five-uniform01.ppm',
'gesture/fig8-3/Point-uniform01.ppm', 'gesture/fig8-3/V-uniform01.ppm',]
figure()
for im in imlist:
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[0]
for i in range(len(imlist)):
sift.plot_features(im, l, True)
title(titlename)
show()
部分图像
接着,利用下面的方法读取训练、测试集的特征和标记信息:
import numpy as np
features,labels = read_gesture_features_labels('gesture/train/')
test_features,test_labels = read_gesture_features_labels('gesture/test/')
classnames = np.unique(labels)
unique()函数可得到排序后的类名称列表。
接着,在数据上使用K近邻代码:
注意:return [os.path.join(path, f) for f in os.listdir(path) if f.endswith(’.ppm’)]
# 测试 KNN
import 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)
但是它并没有告诉我们哪些手势难以分类,或会犯哪些典型错误。混淆矩阵是一个可以显示每类有多少个样本被分在每一类中的矩阵,它可以显示错误的分布情况,以及哪些类是经常相互“混淆”的。
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)
print_confusion(res,labels,classnames)
很直观的混淆矩阵,有37.5%的P被误认为了V。
8.2 贝叶斯分类器
贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分布器,假设特征是彼此独立不相关,贝叶斯分类器在实际应用中获得显著成效(过滤垃圾邮件),另一个优点是,一旦学习了这个模型,就没有必要存储训练数据了,只需存储模型的参数。
该分类是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最高的那个类构造出来的。
首先让我们看使用高斯概率分布模型的贝叶斯分类器基本实现,从训练数据计算得到均值和方差对每个特征单独建模:
from numpy import *
class BayesClassifier(object):
def __init__(self):
""" Initialize classifier with training data. """
self.labels = [] # class labels
self.mean = [] # class mean
self.var = [] # class variances
self.n = 0 # nbr of classes
def train(self,data,labels=None):
""" Train on data (list of arrays n*dim).
Labels are optional, default is 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):
""" Classify the points by computing probabilities
for each class and return most probable label. """
# compute probabilities for each class
est_prob = array([gauss(m,v,points) for m,v in zip(self.mean,self.var)])
print 'est prob',est_prob.shape,self.labels
# get index of highest probability, this gives class label
ndx = est_prob.argmax(axis=0)
est_labels = array([self.labels[n] for n in ndx])
return est_labels, est_prob
def gauss(m,v,x):
""" Evaluate Gaussian in d-dimensions with independent
mean m and variance v at the points in (the rows of) x.
http://en.wikipedia.org/wiki/Multivariate_normal_distribution """
if len(x.shape)==1:
n,d = 1,x.shape[0]
else:
n,d = x.shape
# covariance matrix, subtract mean
S = diag(1/v)
x = x-m
# product of probabilities
y = exp(-0.5*diag(dot(x,dot(S,x.T))))
# normalize and return
return y * (2*pi)**(-d/2.0) / ( sqrt(prod(v)) + 1e-6)
train()方法计算均值和方差的数组列表,classify()计算由高斯函数生成的概率,并选出概率最高的类,最终返回预测的类标记及概率值。
协方差(分析维度之间的线性关系):
用贝叶斯分类器训练,数据为points_normal.pkl:
import pickle
import bayes
import imtools
# 用 Pickle 模块载入二维样本点
with open('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('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])
两个数据集的分类结果,决策边界是椭圆,类似于二维高斯的等值线。
ring:
8.2.1 PCA降维
由于稠密SIFT描述子特征向量参数选取过大,用数据拟合之前进行PCA降维,保留主要成分:
#稠密SIFT
import pca
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])
保持前50维具有最大的方差,通过features中元组与V点乘转换。
8.3 支持向量机
SVM,支持向量机是一类强大的分裂及,最简单的SVM通过在高维空间中寻找一个最优线性分类面,对于特征向量x的决策函数为:
f
(
x
)
=
w
x
=
b
f(x) = wx=b
f(x)=wx=b
w是常规超平面,b是偏移量常数,阈值为0,是一类为正数/负数,求解带有标记的
y
i
∈
(
−
1
,
1
)
y_i\in (-1,1)
yi∈(−1,1)的最优化问题,从而找到决策函数的参数。常规解释训练集上某些特征向量的线性组合:
w
=
∑
i
α
i
y
i
x
i
w=\sum_{i} \alpha _iy_ix_i
w=i∑αiyixi i是训练集中选出的部分样本,称为支持向量,它们可以帮助定义分类的边界。
f
(
x
)
=
∑
i
α
i
y
i
x
i
×
x
−
b
f(x)=\sum_{i} \alpha _iy_ix_i\times x-b
f(x)=i∑αiyixi×x−b
SVM另一个优势是可以使用核函数
K
(
x
i
,
x
)
K(x_i,x)
K(xi,x),将特征向量映射到另一个不同维度的空间中.
常见的核函数:
参数在训练阶段确定的。
8.3.1 使用LibSVM
它是最广泛的SVM实现工具包,下面的脚本会载入在前面kNN 范例分类中用到的数据点,并用径向基函数训练一个 SVM 分类器:
import pickle
from svmutil import *
import imtools
# 用 Pickle 载入二维样本点
with open('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)
调用 svm_train() 求解该优化问题用以确定模型参数,然后就可以用该模型进行预测了。
载入测试集测试:
# 用 Pickle 模块载入测试数据
with open('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])
ring: