文章目录
引言
本章介绍图像分类和图像内容分类算法。首先,我们介绍一些简单而有效的方法和
目前一些性能最好的分类器,并运用它们解决两类和多类分类问题,最后展示两个
用于手势识别和目标识别的应用实例。
8.1K临近分类法(KNN)
在分类算法中,最简单且用的最多的一种方法之一就是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) 进行度量,也可以使用其他度量方式,只需要将其作为函数添加到上面代码的最后。
8.1.1一个简单的二维示例
我们首先建立一些简单的二维示例数据集来说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用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 决策边界适用于没有任何明确模型的类分布。
8.1.2用稠密SIFT作为图像特征
要对图像进行分类,我们需要一个特征向量来表示一幅图像。在整幅图像上用一个规则的网格应用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()
8.1.3图像分类:手势识别
在这个应用中,通过使用稠密SIFT描述子来表示这些手势图像,并建立一个简单的手势识别系统,使用的是书中给出的图像集。
from PCV.localdescriptors import sift, dsift
from pylab import *
import os
from PIL import Image
imlist = ['D:\\picture\\train\\C-uniform02.ppm', 'D:\\picture\\train\\B-uniform01.ppm',
'D:\\picture\\train\\A-uniform01.ppm', 'D:\\picture\\train\\Five-uniform01.ppm',
'D:\\picture\\train\\Point-uniform01.ppm', 'D:\\picture\\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()
实验结果如下
首先对每幅图像创建一个特征文件,文件名后缀为.dift,同时还将图片的分辨率调成了常见的固定大小。
这里定义一个复制函数,用以从文件中读取稠密SIFT描述子:
from pylab import *
import os
from PCV.classifiers import knn
from PCV.localdescriptors import sift, dsift
def read_gesture_features_labels(path):
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,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):
return [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.ppm')]
features,labels = read_gesture_features_labels('D:\\picture\\train\\')
test_features,test_labels = read_gesture_features_labels('D:\\picture\\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))])
# accuracy
acc = sum(1.0*(res == test_labels)) / len(test_labels)
print('Accuracy:', acc)
print_confusion(res,test_labels,classnames)
8.2贝叶斯分类器
另一个简单却有效的分类器是贝叶斯分类器(或称朴素贝叶斯分类器)。贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相关的(这就是它“朴素”的部分)。贝叶斯分类器可以非常有效地被训练出来,原因在于每一个特征模型都是独立选取的。尽管它们的假设非常简单,但是贝叶斯分类器已经在实际应用中获得显著成效,尤其是对垃圾邮件的过滤。贝叶斯分类器的另一个好处是,一旦学习了这个模型,就没有必要存储训练数据了,只需存储模型的参数。
该分类器是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最高的那个类构造出来的。
首先让我们看一个使用高斯概率分布模型的贝叶斯分类器基本实现,也就是用从训练数据集计算得到的特征均值和方差来对每个特征单独建模。把下面的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()
图中不同的颜色代表了类标记,星号代表正确分类,圆点代表错误分类,曲线是分类器的决策边界。
用pca降维
由于稠密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)
8.3支持向量机
SVM(Support Vector Machine,支持向量机)是一类强大的分类器,可以在很多分
类问题中给出现有水准很高的分类结果。最简单的 SVM 通过在高维空间中寻找一
个最优线性分类面,尽可能地将两类数据分开。对于一特征向量 x 的决策函数为:
对于特征向量x的决策函数:
w是常规超平面,b是偏移量常数。函数的月阈值为0,能够很好地将两类数据分开,使其一类为正数,另一类为负数。通过在训练集上求解带有标记的特征向量的最优化问题,使得超平面在两类间具有最大分开间隔,从而找到上面决策函数中的参数w和b。 决策函数的常规解是训练集上某些特征向量的线性组合:
所以决策函数又可以写成:
i是从训练集中选出的部分样本,这个样本称为支持向量。
SVM的一个优势是可以使用核函数,该函数能够将特征向量映射到另一个不同维度的空间中,比如说高维度空间。使用核函数映射可以很有效的解决非线性或者很难的分类问题。
常见的核函数:
1、线性是最简单的情况,即在特征空间中的超平面是线性的
2、多项式用次数为d的多项式对特征进行映射
3、径向基函数,通常指数函数是一种极其有效的选择
4、Sigmoid函数,一个光滑的超平面替代方案
8.3.1使用LibSVM
LibSVM是最好的、使用最为广泛的SVM实现工具包,可以在https://www.lfd.uci.edu/~gohlke/pythonlibs/#libsvm中下在对应版本的whl文件通过pip install文件名的指令进行安装。
下面为LibSVM在二维样本数据点上的运行
import pickle
from libsvm.svmutil import *
from PCV.tools 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')
# 在数据上训练SVM
m = svm_train(prob,param)
# 在数据上分类效果如何
res = svm_predict(labels,samples,m)
使用的是径向基函数训练SVM分类器。以下为得到的结果
不同于前面的例子,这里再载入数据集后要将数组转换成列表,这是因为LibSVM不支持数组对象的输入,使用使用了内建函数map()进行转换,map()函数中用到了对角一个元素都会进行转换的list()函数。创建svm_problem对象,并为期设置一些参数,调用svm_train()求解该优化问题用以确定模型参数,之后就对其进行预测。
下面是核函数类型的介绍:
按照书中代码,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])