Python计算机视觉编程第八章——图像内容分类

本文详细介绍了Python中图像内容分类的三种方法:KNN、贝叶斯分类器和支持向量机(SVM)。KNN算法是非参数、基于实例的懒算法,通过计算与训练样本的距离来分类。贝叶斯分类器利用贝叶斯定理,假设特征独立,适用于垃圾邮件过滤等问题。SVM通过寻找最优超平面实现高维空间的线性分类,能处理非线性问题,核函数的应用使其更具灵活性。文章还展示了各种算法在不同数据集上的应用和效果,并提供了代码示例。
摘要由CSDN通过智能技术生成

(一)K邻近分类法 (KNN)

基本思想:
把待分类文本表示成文本向量,与训练样本组成的样本空间中的向量计算相似度,得到k篇与该文本距离最近(最相似)的文本,根据这k篇文本所属的类别判定新文本所属的类别,在新文本的k个邻居中依次计算每类的权重,将文本分到权重最大的类中。

KNN 是非参数的(non-parametric),基于实例(instance-based)的算法。非参数意味着其不在底层的数据分布上进行任何的臆测。而基于实例意味着其不是明确地学习一个模型,而是选择记忆训练的实例们。由于 KNN 是基于实例的算法,也常被称呼为懒算法(lazy algorithm)。

算法原理:
当 KNN 被用于分类问题时,其输出是一个类别的成员(预测一个类别 - 一个离散值)
该算法包含三个元素:标记对象的集合(比如:一个分数记录的集合),对象之间的距离,k 的取值,即最邻近距离的数量。

我们将要把灰色的点分类为黄色,绿色,蓝色中的一类。一开始会计算灰色点与其他各个点的之间的距离,然后再找出 k 值 - 最邻近的一些点。

最邻近的点的数据按顺序如上所示,黄色的个数最多,所以灰色的点被划分为黄色所在的类。可以发现 KNN 是通过测量不同样本之间的距离进行分类的。KNN 算法的核心思想是:如果一个样本在特征空间中的 k 个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别。

算法步骤:

  1. 依公式计算 Item 与 D1、D2 … …、Dj 之相似度。得到Sim(Item, D1)、Sim(Item, D2)… …、Sim(Item, Dj)。
  2. 将Sim(Item, D1)、Sim(Item, D2)… …、Sim(Item, Dj)排序,若是超过相似度门槛t则放入邻居案例集合NN。
  3. 自邻居案例集合NN中取出前k名,依多数决,得到Item可能类别。

这种方法通常分类效果较好,但是也有很多弊端:与 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([L2dist(point,s) 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) )

def L1dist(v1,v2):
    return sum(abs(v1-v2))

定义一个类并用训练数据初始化非常简单 ; 每次想对某些东西进行分类时,用 KNN 方法,我们就没有必要存储并将训练数据作为参数来传递。用一个字典来存储邻近标记,我们便可以用文本字符串或数字来表示标记。在这个例子中,我们用欧式距离 (L2) 进行度量,也可以使用其他度量方式,只需要将其作为函数添加到上面代码的最后。

一个简单的二维示例

首先建立一些简单的二维示例数据集来说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用 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.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('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
# 用 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)

用不同的保存文件名运行该脚本两次,例如第一次用代码中的文件名进行保存,第二次将代码中的 points_normal_t.pkl 和 points_ring_t.pkl 分别改为 points_normal_test. pkl 和 points_ring_test.pkl 进行保存。将得到 4 个二维数据集文件(如下图所示),上述代码创建了两个不同的二维点集,每个点集有两类。

第一个二维点集中的class_1的数据集原本是200行2列的随机正态分布数据,之后将每个数据缩小了0.6倍。class_2的数据集原本是200行2列的随机正态分布数据,之后将每个数据扩大了1.2倍。

第二个二维点集中的class_1的数据集200行2列的随机正态分布数据,之后将每个数据缩小了0.2倍。class_2数据集是类似于长轴长度为2,短轴长度为根号2的椭圆的分布形状。

每个分布都有两个文件,将一个用来训练,另一个用来做测试。

用下面的代码来创建一个脚本:

# -*- coding: utf-8 -*-
import pickle
from pylab import *
from PCV.classifiers import knn
from PCV.tools import imtools

# 用 Pickle 载入二维数据点
with open('points_normal_t.pkl','r') 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('points_normal_test.pkl', 'r') 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 二维坐标数组和分类器,并返回一个预测的类标记数组。

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')

当n=200 k=3时:
    

当n=20 k=3时:
    

每个示例中,不同颜色代表类标记,正确分类的点用星号表示,分类错误的点用圆点表示,曲线是分类器的决策边界。正如所看到的,kNN 决策边界适用于没有任何明确模型的类分布。

用稠密 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):
    """ 用密集采样的 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 sift, dsift
from pylab import  *
from PIL import Image

dsift.process_image_dsift('D:\\Python\\chapter8\\empire.jpg','D:\\Python\\chapter7\\empire.sift',90,40,True)
l,d = sift.read_features_from_file('D:\\Python\\chapter8\\empire.sift')
im = array(Image.open('D:\\Python\\chapter8\\empire.jpg'))
sift.plot_features(im,l,True)
show()

在这里插入图片描述
代码里面的90,40分别代表圆圈的大小和圆心间隔,可以修改参数调节大小。

图像分类:手势识别

用稠密 SIFT 描述子来表示这些手势图像,将图像放在一个名为 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))

上面代码会对每一幅图像创建一个特征文件,文件名后缀为 .dsift。

注意:这里将图像分辨率调成了常见的固定大小。这是非常重要的,否则这些图像会有不同数量的描述子,从而每幅图像的特征向量长度也不一样,这将导致在后面比较它们时出错。

定义一个辅助函数,用于从文件中读取稠密 SIFT 描述子,如下:

# -*- 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):
    return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.ppm')]

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)
    #读取训练集、测试集的特征和标记信息
features,labels = read_gesture_features_labels('D:\\Python\\chapter8\\train\\')
test_features,test_labels = read_gesture_features_labels('D:\\Python\\chapter8\\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)

首先,用训练数据及其标记作为输入,创建分类器对象;然后,我们在整个测试集上遍历并用 classify() 方法对每幅图像进行分类。将布尔数组和 1 相乘并求和,可以计算出分类的正确率。由于该例中真值为 1,所以很容易计算出正确分类数。理论上应该会打印出一个类似下面的结果:

Accuracy: 0.811518324607

训练集和测试集完全相同会打印
在这里插入图片描述
但是训练集和测试集完全不相同也会打印
在这里插入图片描述
期间换了各种手势图片以及K值依旧如此,如果有博主知道原因,可以解答一下这个现象,谢谢!

(二)贝叶斯分类器

贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相关的 (这就是它“朴素”的部分)。贝叶斯分类器可以非常有效地被训练出来,原因在于每一个特征模型都是独立选取的。尽管它们的假设非常简单,但是贝叶斯分类器已经在实际应用中获得显著成效,尤其是对垃圾邮件的过滤

贝叶斯公式

P ( A ∣ B ) = P ( B / A ) P ( A ) P ( B ) P(A|B)=\frac{P(B/A)P(A)}{P(B)} P(AB)=P(B)P(B/A)P(A)

拼写纠错实例:

问题:我们看到用户输入了一个不在字典中的单词,我们需要去猜测,用户真正想输入的单词是什么?

  1. 转换成数学语言:
    P(我们猜测他想输入的单词 | 他实际输入的单词 )

  2. 假设用户实际输入的单词记为D(D代表data,即为观测数据)

  3. 猜测1: P ( h 1 ∣ D ) P(h1 | D) P(h1D),猜测2: P ( h 2 ∣ D ) P(h2 | D) P(h2D),猜测3: P ( h 3 ∣ D ) P(h3 | D) P(h3D)…统一为: P ( h ∣ D ) P(h | D) P(hD)
    P ( h ∣ D ) = P ( D ∣ h ) ∗ P ( h ) / P ( D ) P(h|D)=P(D|h)*P(h)/P(D) P(hD)=P(Dh)P(h)/P(D)

  4. 对于不同的具体猜测 h 1 , h 2 , h 3.... p ( D ) h1,h2,h3....p(D) h1,h2,h3....p(D)都是一样的,所以在比较 P ( h 1 ∣ D ) P(h1|D) P(h1D) P ( h 2 ∣ D ) P(h2|D) P(h2D)的时候我们可以忽略这个常数。则 P ( h ∣ D ) P(h|D) P(hD)正比于 P ( D ∣ h ) ∗ P ( h ) P(D|h)*P(h) P(Dh)P(h)

  5. 对于给定观测数据,一个猜测是好是坏,取决于“这个猜测本身独立的可能性大小(先验概率,Prior)” 和 “这个猜测生成我们观测到的数据的可能性大小”

  6. 贝叶斯方法计算: P ( D ∣ h ) ∗ P ( h ) P(D|h)*P(h) P(Dh)P(h) P ( h ) P(h) P(h)是特定猜测的先验概率。比如用户输入tIp,那到底是top还是tip,这个时候,当最大似然(最符合观测数据的(即 P ( D ∣ h ) P(D|h) P(Dh)最大的)最有优势。)不能做出决定性的判断时,先验概率就可以给出指示,一般来说top出现的程度要高许多,所以他更可能打的是top。

贝叶斯拼写检查器

它以一个单词作为输入参数, 返回最可能的拼写建议结果

import re, collections
 # 把语料中的单词全部抽取出来, 转成小写, 并且去除单词中间的特殊符号
def words(text): return re.findall('[a-z]+', text.lower()) 
 
def train(features):
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
    return model
 
NWORDS = train(words(open(r'D:\\Python\\chapter8\\big.txt').read()))
 
alphabet = 'abcdefghijklmnopqrstuvwxyz'
#返回所有与单词 w 编辑距离为 1 的集合 
def edits1(word):
    n = len(word)
    return set([word[0:i]+word[i+1:] for i in range(n)] +                     # deletion
               [word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)] + # transposition
               [word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet] + # alteration
               [word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet])  # insertion
               
#返回所有与单词 w 编辑距离为 2 的集合
#在这些编辑距离小于2的词中间, 只把那些正确的词作为候选词 
def known_edits2(word):
    return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)
 
def known(words): return set(w for w in words if w in NWORDS)
#如果known(set)非空, candidate 就会选取这个集合, 而不继续计算后面的
def correct(word):
    candidates = known([word]) or known(edits1(word)) or known_edits2(word) or [word]
    return max(candidates, key=lambda w: NWORDS[w])


correct('lovele')
#correct('speling')

代码运行结果:
在这里插入图片描述
在这里插入图片描述
垃圾邮件过滤实例:

问题:给定一封邮件,判定它是否属于垃圾邮件?

  1. D D D来表示这封邮件,由 N N N个单词组成。用 S S S表示垃圾邮件, H H H表示正常邮件。
    P ( S ∣ D ) = P ( S ) ∗ P ( D ∣ S ) / P ( D ) P(S|D)=P(S)*P(D|S)/P(D) P(SD)=P(S)P(D

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值