八、【python计算机视觉编程】图像内容分类

本篇博客主要介绍图像分类和图像内容分类算法,一些简单而有效的方法和目前一些性能最好的分类器,并运用它们解决两类和多类分类问题。

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

在分类方法中,最简单且用得最多的一种方法之一就是KNN(K邻近分类法),这种算法把要分类的对象(例如一个特征向量)与训练集中已知类标记的所有对象进行对比,并由k近邻对指派到哪个类进行投票。 这种方法通常分类效果比较好,但是也有很多弊端:与K-means聚类算法一样,需要预先设定k值,k值得选择会影响分类的性能;此外,这种方法要求将整个训练集存储起来,如果训练集过大,搜索效率就非常慢。对于这种情况,就采用某些装箱形式通常会减少对比的次数。这种算法的可并行性一般。KNN算法是机器学习的一种入门算法。

KNN算法涉及3个主要因素:

  • 训练集
  • 距离或相似的衡量
  • K的大小

1.训练集的指导思想:
“近朱者赤,近墨者黑”,根据你的邻近位置的类别来推断出你的类别。

计算步骤如下:
1)算距离:给定测试对象,计算它与训练集中的每个对象的距离。
2)找邻居:圈定距离最近的k个训练对象,作为测试对象的近邻。
3)做分类:根据这k个近邻归属的主要类别,来对测试对象分类。

2.距离或相似度的衡量
什么是合适的距离衡量?距离越近应该意味着这两个点属于一个分类的可能性越大。
距离衡量包括欧式距离(Euclidean Distance)、余弦值(cos)、相关度(correlation)、曼哈顿距离(Manhattan Diatance)等。最常见的计算各点之间的方法是欧氏距离(Euclidean Distance)。欧氏距离就是计算 N 维空间中两点之间的距离。
其中,欧氏距离的计算公式为: d = ∑ i = 1 N ( x 1 i − x 2 i ) 2 d=\sqrt{\sum_{i=1}^{N}(x_{1i}-x_{2i})^{2}} d=i=1N(x1ix2i)2
曼哈顿距离计算公式为: d ( x , y ) = ∑ k = 1 n ∣ x k − y k ∣ d(x,y)=\sqrt{\sum_{k=1}^{n}\begin{vmatrix} x_{k}-y_{k} \end{vmatrix}} d(x,y)=k=1nxkyk
对于文本分类来说,使用余弦(cosine)来计算相似度就比欧式(Euclidean)距离更合适。

3.K的大小设定:
k太小,分类结果易受噪声点影响;k太大,近邻中又可能包含太多的其它类别的点。(对距离加权,可以降低k值设定的影响),k值通常是采用交叉检验来确定(以k=1为基准)。
通常来说,k一般低于训练样本数的平方根

4.类别的判定
投票决定: 少数服从多数,近邻中哪个类别的点最多就分为该类。
加权投票法: 根据距离的远近,对近邻的投票进行加权,距离越近则权重越大(权重为距离平方的倒数)。

5.KNN算法的优缺点:
优点:
简单,易于理解,易于实现,无需估计参数,无需训练;
适合对稀有事件进行分类(例如当流失率很低时,比如低于0.5%,构造流失预测模型);
特别适合于多分类问题(multi-modal,对象具有多个类别标签),例如根据基因特征来判断其功能分类,kNN比SVM的表现要好。
缺点:
懒惰算法,对测试样本分类时的计算量大,内存开销大,评分慢;
可解释性较差,无法给出决策树那样的规则。

6.改进KNN算法:
考虑距离,根据距离加上权重。 比如:1/d(d为距离)。

(1)一个简单的二维示例

算法实现:
首先将添加一个名称为knn.py的文件,用于完成将给定训练样本集和对应的标记列表,这些训练样本和标记可以在一个数组里成行摆放或者直接摆放在列表里,训练样本可以是数字、字符串等。 定义一个类并用训练数据初始化非常简单,每次想对某些东西进行分类时,用KNN方法,不需要存储并将训练数据作为参数来传递。用一个字典来存储邻近标记,便可以用文本字符串或数字来表示标记。

# -*- coding: utf-8 -*-
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, key=lambda x: votes.get(x))


def L2dist(p1,p2):
    return sqrt( sum( (p1-p2)**2) )

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

编写实验代码:

# -*- coding: utf-8 -*-

from numpy.random import randn
import pickle
from pylab import *

# 创建二维样本数据
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', '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)
# 正态分布,并使数据成环绕状分布
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)))
# 用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!")

用不同的保存文件名运行该代码两次,例如第一次用points_normal_t.pkl和points_ring_pkl,则第二次改为points_normal_test.pkl和points_ring_test.pkl进行保存。得到4个二维数据集文件,每个分布都有两个文件,一个用来训练,一个用来做测试。
在这里插入图片描述

# -*- coding: utf-8 -*-
import pickle
from pylab import *
from PCV.classifiers import knn
from PCV.tools import imtools
import matplotlib.pyplot as plt
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

pklist=['points_normal.pkl','points_ring.pkl']

figure()

# load 2D points using Pickle
for i, pklfile in enumerate(pklist):
    with open('points_normal_t.pkl', 'rb') as f:
        class_1 = pickle.load(f)
        class_2 = pickle.load(f)
        labels = pickle.load(f)
    # load test data using 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)
    savefig("test1.png")
show()

实验结果:
当n=200时,
在这里插入图片描述
当n=40时,
在这里插入图片描述
分析:
从实验结果显示,不同颜色代表不同类的标记。正确分类的点用星号表示,分类错误的点用圆点表示,曲线是分类器的决策边界。二维数据样本越多,所显示的分类效果越好,运行速度会很慢;反之当样本减少时,分类效果会差一些,但速度会变快。在上面代码中所用的函数需要一个决策函数(分类器),并且用meshgird()函数在一个网格上进行预测。决策函数的等值线可以显示边界的位置,默认边界为零等值线。

(2)用稠密SIFT作为图像特征

在整幅图像上用一个规则的网格应用SIFT描述子可以得到稠密SIFT的表示形式,可以通过添加一些额外的参数来得到稠密SIFT特征。
首先,说一说稠密sift和之前说的sift的区别,

稠密SIFT是一种用特征进行目标跟踪的算法。 这个算法首先将表达目标的矩形区域分成相同大小的矩形块,计算每一个小块的SIFT特征,再对各个小块的稠密SIFT特征在中心位置进行采样,建模目标的表达。然后度量两个图像区域的不相似性,先计算两个区域对应小块的Bhattacharyya距离,再对各距离加权求和作为两个区域间的距离。
因为目标所在区域靠近边缘的部分可能受到背景像素的影响,而区域的内部则更一致,所以越靠近区域中心权函数的值越大。最后提出了能适应目标尺度变化的跟踪算法。该算法具有良好的性能。DSIFT算法是在SIFT算法上进行改进的算法。

之前提到的SIFT算法没有将图像的特征分成多个矩形块采样,而是直接将每个像素都作为可能的特征,通过得到的特征描述子作为图像的特征点。

编写实验代码:

# -*- coding: utf-8 -*-
from PCV.localdescriptors import sift, dsift
from pylab import  *
from PIL import Image

dsift.process_image_dsift('gesture/empire.jpg','empire.dsift',200,10,True)
l,d = sift.read_features_from_file('empire.dsift')
im = array(Image.open('gesture/empire.jpg'))
sift.plot_features(im,l,True)
title('Dense SIFT')
show()

实验结果:
在这里插入图片描述

遇到的问题:
在这里插入图片描述
这个问题还没有能够解决,希望有大神能指导指导,谢谢!

(3)图像分类:手势识别

在这个应用中,会用稠密SIFT描述子来表示这些手势图像,并建立一个简单的手势识别系统。用上面的稠密SIFT函数对图像进行处理,可以得到所有图像的特征向量。这里,假设列表imlist中包含了所有图像的文件名,通过下面的代码得到每幅图像的稠密SIFT特征:

import dsift
for i, im in enumerate(imlist):
    print (im)
    dsift.process_image_dsift(im,im[:-3]+'dsift',90,40,True)
    l,d = sift.read_features_from_file(im[:-3]+'dsift')
    dirpath, filename=os.path.split(im)
    im = array(Image.open(im))

上面的代码会对每一幅图像创建一个特征文件,文件名后缀为.dsift。注意,这里将图像分辨率调成了常见的固定大小。

编写手势识别的实验代码:

# -*- 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',90,40,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()

实验结果:
在这里插入图片描述
在这里插入图片描述
定义一个辅助函数,用于从文件中读取稠密描述子:

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)

然后用下面的脚本读取训练集、测试集的特征和标记信息:

features,labels = read_gesture_features_labels('gesture/train/')
test_features,test_labels = read_gesture_features_labels('gesture/test/')
classnames = unique(labels)

在数据上使用前面的K近邻代码:

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

首先,用训练数据极其标记作为输入,创建分类器对象,然后在整个测试集上遍历并用classsify()方法对每幅图像进行分类。将布尔数组和1相乘并求和,可以计算出分类的正确率。由于该例中真值为1,所以很容易计算出正确分类数,打印出以下结果:
在这里插入图片描述
这说明该例中有81.3%的图像是正确的。

虽然上面的正确率显示了一个对于给定的测试集有多少图像是正确分类的,但是它并没有告诉我们哪些手势难以分类,或会有哪些典型错误,混淆矩阵是一个可以显示每个类有多少样本被分在哪一类矩阵中,可以显示错误的分布情况。

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,test_labels,classnames)

在这里插入图片描述
上面混淆矩阵显示,P常常被误分为V。

(二)贝叶斯分类器

贝叶斯分类器是各种分类器中分类错误概率最小或者在预先给定代价的情况下平均风险最小的分类器。贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相关的。贝叶斯分类器可以有效地被训练出来,是由于每一个特征模型都是独立选取的。尽管它们的假设非常简单,但是贝叶斯分类器已经在实际应用中获得显著成效,尤其是对垃圾邮件的过滤。贝叶斯分类器的另一个好处是,一旦学习了这个模型,就没有必要存储训练数据了,只需要存储模型的参数。该分类器是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最高的那个类构造出来。

来回忆一下贝叶斯条件概率的数学公式: P ( B i ∣ A ) = P ( B i ) P ( A ∣ B i ) ∑ j = 1 n P ( B j ) P ( A ∣ B j ) P(B_{i}|A)=\frac{P(B_{i})P(A|B_{i})}{\sum_{j=1}^{n}P(B_{j})P(A|B_{j})} P(BiA)=j=1nP(Bj)P(ABj)P(Bi)P(ABi)
公式描述: 公式中,事件Bi的概率为P(Bi),事件Bi已发生条件下事件A的概率为P(A│Bi),事件A发生条件下事件Bi的概率为P(Bi│A)

贝叶斯分类器的应用:垃圾邮件的过滤

1.提出问题: 垃圾邮件是一种令人头痛的顽症,困扰着所有的互联网用户。正确识别垃圾邮件的技术难度非常大。传统的垃圾邮件过滤方法,主要有“关键词法”和“校验码法”等。前者的过滤依据是特定的词语;后者则是计算邮件文本的校验码,再与已知的垃圾邮件进行对比。它们的识别效果都不理想,而且很容易规避。因此,Paul Graham提出用贝叶斯过滤器用于过滤垃圾邮件。

2.实验准备: 贝叶斯过滤器是一种统计学过滤器,建立在已有的统计结果之上。所以,在实验之前,必须预先提供两组已经识别好的邮件,一组是正常邮件,另一组是垃圾邮件我们用这两组邮件,对过滤器进行“训练”。这两组邮件的规模越大,训练效果就越好。“训练”过程很简单。首先,解析所有的邮件,提取每一个词。 然后,计算每个词语在正常邮件和垃圾邮件中出现的频率

3.使用过程:

前提: 假设邮件的总数为正常邮件和垃圾邮件各4000封。我们假定“sex”这个词,在4000封垃圾邮件中,有200封包含这个词,那么它的出现频率就是5%;而在4000封正常邮件中,只有2封包含这个词,那么出现频率就是0.05%。
假设: 我们收到了一封信邮件。在未统计分析之前, 我们假定它是垃圾邮件的概率为50%。我们用S表示垃圾邮件(spam),H表示正常邮件(healthy)。因此,P(S)和P(H)的先验概率,均为50%。

P ( S ) = P ( H ) = 50 P(S)=P(H)=50 P(S)=P(H)=50%

然后,对这封邮件进行解析,发现其中包含了“sex”这个词,请问这封邮件属于垃圾邮件的概率有多高?

我们用W表示“sex”这个词,问题就变成了如何计算P(S|W)的值,即在某个词语(W)已经存在的情况下,垃圾邮件(S)的概率有多大?
根据条件概率公式, 可以写出: P ( S ∣ W ) = P ( W ∣ S ) P ( S ) P ( W ∣ S ) P ( S ) + P ( W ∣ H ) P ( H ) P(S|W)=\frac{P(W|S)P(S)}{P(W|S)P(S)+P(W|H)P(H)} P(SW)=P(WS)P(S)+P(WH)P(H)P(WS)P(S)
因此,这封新邮件是垃圾邮件的概率等于99%。这说明,sex这个词的推断能力很强,将50%的“先验概率”一下子提高到了99%的“后验概率”。

那么现在就可以说明,这封新邮件就是垃圾邮件吗?答案是不能的。因为一封邮件包含很多词语,一些词语(比如sex)说明这是垃圾邮件,另一些说不是。你怎么知道以哪个词为准呢?Paul GraHam的做法是:选出这封邮件中P(S|W)最高的15个词,计算它们的联合概率。

下面就引入了联合概率的概念:所谓联合概率,就是指在多个事件发生的情况下,另一个事件发生的概率有多大。比如,已知W1和W2是两个不同的词语,它们都出现在某封电子邮件之中,那么根据W1和W2的概率来判断这封邮件是垃圾邮件的概率,就是联合概率。

在已知W1和W2的情况下,会产生两种结果:垃圾邮件(事件E1)或正常邮件(事件E2)。

EventW1W2Junk Mail
E1AppearAppearYes
E2AppearAppearNo

其中,W1、W2和垃圾邮件的概率分别如下:

EventW1W2Junk Mail
E1P(S/W1)P(S/W2)P(S)
E21-P(S/W1)1-P(S/W2)1-P(S)

如果假定所有事件都是独立事件,那么就可以计算P(E1)和P(E2):
P ( E 1 ) = P ( S ∣ W 1 ) P ( S ∣ W 2 ) P ( S ) P(E_{1})=P(S|W_{1})P(S|W_{2})P(S) P(E1)=P(SW1)P(SW2)P(S)
P ( E 2 ​ ) = [ 1 − P ( S ∣ W 1 ​ ) ] [ 1 − P ( S ∣ W 2 ​ ) ] [ 1 − P ( S ) ] P(E_{2}​)=[1−P(S∣W1​)][1−P(S∣W2​)][1−P(S)] P(E2)=[1P(SW1)][1P(SW2)][1P(S)]
又由于在W1和W2已经发生的情况下,垃圾邮件的概率等于下面公式: P = P ( E 1 ) P ( E 1 ) + P ( E 2 ) P=\frac{P(E_{1})}{P(E_{1})+P(E_{2})} P=P(E1)+P(E2)P(E1)
上面的公式说明,E1是在W1和W2同时出现的情况下垃圾邮件的事件,E2是W1和W2同时出现的情况下正常邮件的事件,注意这里的前提都是 “在W1和W2同时出现的情况下”。 这个公式的意思就是,W1和W2共同出现的情况下是垃圾邮件的概率,而P(W1,W2)实际上就是P(E1)+P(E2),所以P = P(E1)/(P(E1)+P(E2))。

即, P = P ( S ∣ W 1 ) P ( S ∣ W 2 ) P ( S ) P ( S ∣ W 1 ) P ( S ∣ W 2 ) P ( S ) + ( 1 − P ( S ∣ W 1 ​ ) ) ( 1 − P ( S ∣ W 2 ​ ) ) ( 1 − P ( S ) ) P=\frac{P(S|W_{1})P(S|W_{2})P(S)}{P(S|W_{1})P(S|W_{2})P(S)+(1−P(S∣W_{1}​))(1−P(S∣W_{2}​))(1−P(S))} P=P(SW1)P(SW2)P(S)+(1P(SW1))(1P(SW2))(1P(S))P(SW1)P(SW2)P(S)
将P(S)=0.5代入,得到, P = P ( S ∣ W 1 ) P ( S ∣ W 2 ) P ( S ∣ W 1 ) P ( S ∣ W 2 ) P ( S ) + ( 1 − P ( S ∣ W 1 ​ ) ) ( 1 − P ( S ∣ W 2 ​ ) ) ) P=\frac{P(S|W_{1})P(S|W_{2})}{P(S|W_{1})P(S|W_{2})P(S)+(1−P(S∣W1​))(1−P(S∣W2​)))} P=P(SW1)P(SW2)P(S)+(1P(SW1))(1P(SW2)))P(SW1)P(SW2)这就是联合概率的计算公式。
将上面的公式扩展到15个词的情况,就得到了最终的概率计算公式:
P = P 1 P 2 P 3 ⋯ P 15 P 1 P 2 P 3 ⋯ P 15 + ( 1 − P 1 ) ( 1 − P 2 ) ⋯ ( 1 − P 15 ) P=\frac{P_{1}P_{2}P_{3}\cdots P_{15}}{P_{1}P_{2}P_{3}\cdots P_{15}+(1-P_{1})(1-P_{2})\cdots (1-P_{15})} P=P1P2P3P15+(1P1)(1P2)(1P15)P1P2P3P15
一封邮件是不是垃圾邮件,就用这个式子进行计算。这时还需要一个用于比较的门槛值。Paul Graham的门槛值是0.9,概率是0.9,表示15个词联合认定,这封邮件有90%以上的可能属于垃圾邮件;概率小于0.9,就表示是正常邮件。

有了这个公式以后,一封正常的邮件即使出现了sex这类词语,也不会被认定为垃圾邮件了。

下面看一个使用高斯概率分布模型的贝叶斯分类器基本实现,也就是从训练数据集计算得到的特征均值和方差来对每个特征单独建模。把下面的BayesClassifier类添加到文件 bayes.py 中:

# -*- coding: utf-8 -*-
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的概率。

将该贝叶斯分类器用于上一节的二维数据,下面的脚本将载入上一节的二维数据,并训练出一个分类器:


# -*- coding: utf-8 -*-
import pickle
import bayes
import matplotlib.pyplot as plt
import imtools
import numpy as np

#用pickle模块载入二维样本点
with open('points_normal_t.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])
show()

将脚本前10个二维数据点的分类结果打印输出到控制台,结果如下:
在这里插入图片描述

用PCA降维

这部分内容可以参考 PCA降维
概念:
PCA(principal Component Analysis),主成分分析法。顾名思义,就是提取出数据中主要的成分,是一种数据压缩方法,常用于去除噪声、数据预处理,也是机器学习中常见的降维方法。

基本思想:

  • 去除平均值
  • 计算协方差矩阵
  • 计算协方差矩阵的特征值和特征向量
  • 将特征值排序
  • 保留前N个最大的特征值对应的特征向量
  • 将数据转换到上面得到的N个特征向量构建的新空间中(实现了特征压缩)

原理:

  1. 找出第一个主成分的方向,也就是数据方差最大的方向。
  2. 找出第二个主成分的方向,也就是数据方差次大的方向,并且该方向与第一个主成分方向正交(orthogonal, 如果是二维空间就叫垂直)。
  3. 通过这种方式计算出所有的主成分方向。
  4. 通过数据集的协方差矩阵及其特征值分析,我们就可以得到这些主成分的值。
  5. 一旦得到了协方差矩阵的特征值和特征向量,我们就可以保留最大的 N 个特征。这些特征向量也给出了 N 个最重要特征的真实结构,我们就可以通过将数据乘上这 N 个特征向量 从而将它转换到新的空间上。

PCA 优点: 降低数据的复杂性,识别最重要的多个特征。
PCA缺点: 不一定需要,且可能损失有用信息。

适用数据类型: 数值型数据。

用PCA降维步骤:
求解数据集的主成分的过程,即为求解样本集协方差矩阵的特征值与特征向量的过程。

假设数据集为:
X = [ X 1 X 2 ] = [ − 1 − 1 0 2 0 − 2 0 0 1 1 ] X=\begin{bmatrix} X_{1}\\ X_{2} \end{bmatrix}=\begin{bmatrix} -1 &-1 &0 & 2 & 0\\ -2 & 0 & 0 & 1 & 1 \end{bmatrix} X=[X1X2]=[1210002101]

  1. 将样本数据中心化(为了简化运算,这部并不是必须的):上述数据集已完成中心化。
  2. 计算样本数据的协方差矩阵。
    ∑ = X X T = 1 5 [ − 1 − 1 0 2 0 − 2 0 0 1 1 ] [ − 1 − 2 − 1 0 0 0 2 1 0 1 ] = [ 6 5 4 5 4 5 6 5 ] \sum =XX^{T}=\frac{1}{5}\begin{bmatrix} -1 &-1 &0 & 2 & 0\\ -2 & 0 & 0 & 1 & 1 \end{bmatrix}\begin{bmatrix} -1 & -2\\ -1 & 0\\ 0 & 0\\ 2 & 1\\ 0 & 1 \end{bmatrix}=\begin{bmatrix} \frac{6}{5} & \frac{4}{5}\\ \frac{4}{5} & \frac{6}{5} \end{bmatrix} =XXT=51[1210002101]1102020011=[56545456]
  3. 求出协方差矩阵的特征值与正交单位特征向量。
    特征值: λ 1 = 2 \lambda _{1}=2 λ1=2,对应的特征向量为: μ 1 = [ 1 2 1 2 ] \mu _{1}=\begin{bmatrix} \frac{1}{\sqrt{2}}\\ \frac{1}{\sqrt{2}} \end{bmatrix} μ1=[2 12 1]
    特征值: λ 2 = 2 / 5 \lambda _{2}=2/5 λ2=2/5,对应的特征向量为: μ 2 = [ − 1 2 1 2 ] \mu _{2}=\begin{bmatrix} -\frac{1}{\sqrt{2}}\\ \frac{1}{\sqrt{2}} \end{bmatrix} μ2=[2 12 1]
  4. 对上述协方差矩阵对角化。
    P T ∑ P = [ 1 2 1 2 − 1 2 1 2 ] ⋅ [ 6 5 4 5 4 5 6 5 ] ⋅ [ 1 2 − 1 2 1 2 1 2 ] = [ 2 0 0 2 / 5 ] P^{T}\sum P=\begin{bmatrix} \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}}\\ -\frac{1}{\sqrt{2}}& \frac{1}{\sqrt{2}} \end{bmatrix}\cdot \begin{bmatrix} \frac{6}{5} & \frac{4}{5}\\ \frac{4}{5} & \frac{6}{5} \end{bmatrix}\cdot \begin{bmatrix} \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}}\\ \frac{1}{\sqrt{2}}& \frac{1}{\sqrt{2}} \end{bmatrix}= \begin{bmatrix} 2 & 0\\ 0& 2/5 \end{bmatrix} PTP=[2 12 12 12 1][56545456][2 12 12 12 1]=[2002/5]
    最大特征值为 λ 1 = 2 \lambda _{1}=2 λ1=2,对应的特征向量为 μ 1 = [ 1 2 1 2 ] \mu _{1}=\begin{bmatrix} \frac{1}{\sqrt{2}}\\ \frac{1}{\sqrt{2}} \end{bmatrix} μ1=[2 12 1],故第一主成分为: y 1 = [ 1 2 , 1 2 ] [ − 1 − 1 0 2 0 − 2 0 0 1 1 ] = [ − 3 2 − 1 2 0 3 2 − 1 2 ] y_{1}=\begin{bmatrix} \frac{1}{\sqrt{2}} ,& \frac{1}{\sqrt{2}} \end{bmatrix}\begin{bmatrix} -1 &-1 &0 & 2 & 0\\ -2 & 0 & 0 & 1 & 1 \end{bmatrix}=\begin{bmatrix} -\frac{3}{\sqrt{2}} & -\frac{1}{\sqrt{2}} & 0 & \frac{3}{\sqrt{2}} & -\frac{1}{\sqrt{2}} \end{bmatrix} y1=[2 1,2 1][1210002101]=[2 32 102 32 1]
    次大特征值为 λ 2 = 2 / 5 \lambda _{2}=2/5 λ2=2/5,对应的特征向量为 μ 2 = [ − 1 2 1 2 ] \mu _{2}=\begin{bmatrix} -\frac{1}{\sqrt{2}}\\ \frac{1}{\sqrt{2}} \end{bmatrix} μ2=[2 12 1],故第二主成分为: y 2 = [ − 1 2 , 1 2 ] [ − 1 − 1 0 2 0 − 2 0 0 1 1 ] = [ − 1 2 1 2 0 − 1 2 1 2 ] y_{2}=\begin{bmatrix} -\frac{1}{\sqrt{2}} ,& \frac{1}{\sqrt{2}} \end{bmatrix}\begin{bmatrix} -1 &-1 &0 & 2 & 0\\ -2 & 0 & 0 & 1 & 1 \end{bmatrix}=\begin{bmatrix} -\frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} & 0 & -\frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} \end{bmatrix} y2=[2 1,2 1][1210002101]=[2 12 102 12 1]

降维后的图像如下图所示:
在这里插入图片描述
上面手势识别的实验中,由于稠密SIFT描述子的特征向量十分庞大,在用数据拟合模型之前,进行降维处理可以更好的进行实验。下面的脚本就用 pca.py 中的PCA进行降维:


# -*- coding: utf-8 -*-
import pickle
import bayes
import matplotlib.pyplot as plt
import imtools
import numpy as np
from numpy import * 
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])

#测试贝叶斯分类器
bc = bayes.BayesClassifier()
blist = [features[where(labels==c)[0]] for c in classnames]

bc.train(blist,classnames)
res = bc.classify(test_features)[0]

BayesClassifier需要获取数组列表(每一类对应一个数组),把数据传递给train()函数之前,需要对数据进行转换。还不需要概率,只需要返回预测的类标记。

acc = sum(1.0*(res==test_labels))/len(test_labels)
print('Accuracy:',acc)

输出结果:

Accuracy:0.717277486911

检查混淆矩阵:

print_confusion(res,test_labels,classnames)

输出结果是:

这里是引用

(三)支持向量机SVM

概念:
支持向量机(Support Vector Machine, SVM)是一类按监督学习(supervised learning)方式对数据进行二元分类(binary classification)的广义线性分类器(generalized linear classifier),其决策边界是对学习样本求解的最大边距超平面(maximum-margin hyperplane)。最简单的SVM是 通过在高维空间中寻找一个最优线性分类面,尽可能地将两类数据分开。
对于一特征向量 x x x的决策函数为: f ( x ) = w ⋅ x − b f(x)=w\cdot x-b f(x)=wxb
其中, w w w是常规的超平面, b b b是偏移量常数。该函数月阈值为0,它能够很好地将两个数据分开,使其一类为正数,另一类为负数。通过在训练集上求解那些带有标记 y i ∈ { − 1 , 1 } y_{i}\in \begin{Bmatrix} -1,1 \end{Bmatrix} yi{1,1}的特征向量xi的最优化问题,使超平面在两类间具有最大分开间隔,从而找到上面决策函数中的参数 w w w b b b。该决策函数的常规解释训练集上某些特征向量的线性组合: w = ∑ i α i y i x i w=\sum_{i}^{ }\alpha _{i}y_{i}x_{i} w=iαiyixi
所以决策函数可以写为: f ( x ) = ∑ i α i y i x i ⋅ x − b f(x)=\sum_{i}^{ }\alpha _{i}y_{i}x_{i}\cdot x-b f(x)=iαiyixixb这里的 i i i是从训练集中选出的部分样本,这里选择的样本称为支持向量,它们可以帮助定义分类的边界。

SVM 基本原理:
SVM 原理分为软间隔最大化、拉格朗日对偶、最优化问题求解、核函数、序列最小优化 SMO 等部分。具体的原理不在这里叙述了,深入学习可以参考 https://www.ibm.com/developerworks/cn/analytics/library/machine-learning-hands-on1-svn/index.html 。

SVM支持向量机由简至繁的模型包括:

  • 当训练样本线性可分时,通过硬间隔最大化,学习一个线性可分支持向量机;
  • 当训练样本近似线性可分时,通过软间隔最大化,学习一个线性支持向量机;
  • 当训练样本线性不可分时,通过核技巧和软间隔最大化,学习一个非线性支持向量机。

SVM 的优点是:

  1. 可以解决线性不可分的情况。
  2. 计算复杂度仅取决于少量支持向量,对于数据量大的数据集计算复杂度低。

(下面展示线性不可分的情况)
在这里插入图片描述
SVM 的缺点是:

  1. 经典的 SVM 算法仅支持二分类,对于多分类问题需要改动模型;
  2. 不支持类别型数据,需在预处理阶段将类别型数据转换成离散型数据。类别型数据即"男"、 "女"这类由字符串表示某类信息的数据,需将这类数据转换成离散型数据如 1、2。

SVM的一个优势是可以使用核函数;核函数能够将特征向量映射到另外一个不同维度的空间中,比如高维度空间。通过核函数映射,可以保持对决策函数的控制,可以有效地解决非线性或者很难的分类问题。用核函数 K ( x i , x ) K(x_{i},x) K(xi,x)替代上面决策函数中的内积 x i ⋅ x x_{i}\cdot x xix
在这里插入图片描述
(来自百度百科)

安装LibSVM和gnuplot

LibSVM为python提供了一个良好的接口,关于安装和应用可以参考 ,需要下载libsvm和gnuplot 。
https://www.cnblogs.com/bbn0111/p/8318629.html
https://blog.csdn.net/weixin_42014622/article/details/82962634

支持向量机的实现:

  1. 绘制散点图:make_blobs()
    其中,参数n_samples:一共多少样本点;centers:分为多少簇;
    random_state:确定随机种子,保证每次的数据都是一样的##
    cluster_std:每个簇的离散程度,越大越分散,越小越集中更好分类。
  2. 绘制分割线

(线性)

from SVM import SVM
from sklearn import datasets
import matplotlib.pyplot as plt
blobs=datasets.make_blobs(n_samples=100,centers=2,cluster_std=2)
X=blobs[0]
y=blobs[1]
y=[1 if i==1 or i==2 else -1 for i in y]
svm=SVM(toler=0.01,C=1,kernel_option=('linear',0))
svm.fit(X,y)
y_predict=svm.predict(X)
plt.scatter(X[:,0],X[:,1],c=y_predict)
svm.show_graph()

实验结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
多运行几次上面的代码,会显示不同的结果。实验结果显示,每一次都根据不同的线分类出不同的结果,结果可能会重复出现,也可能会覆盖之前的分类点,会干扰上一次的结果。

(非线性)

# -*- coding: utf-8 -*-
from svm import SVM
from sklearn import datasets
import matplotlib.pyplot as plt
plt.rcParams['axes.unicode_minus'] = False
moon=datasets.make_moons(n_samples=200, noise=0.05, random_state=0)
X=moon[0]
y=moon[1]
y=[1 if i==1 or i==2 else -1 for i in y]
svm=SVM(toler=0.01,C=1,kernel_option=('linear',0))
svm.fit(X,y)
y_predict=svm.predict(X)
plt.scatter(X[:,0],X[:,1],c=y_predict)
svm.show_graph()

在这里插入图片描述
非线性的情况下,运行多次得出的结果还是上如图所示。

(四)光学字符识别

概念:
OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程;即,针对印刷体字符,采用光学的方式将纸质文档中的文字转换成为黑白点阵的图像文件,并通过识别软件将图像中的文字转换成文本格式,供文字处理软件进一步编辑加工的技术。

实现验证码识别

编写实验代码:

# coding:utf-8
from PIL import Image,ImageEnhance
import pytesseract
#上面都是导包,只需要下面这一行就能实现图片文字识别
im = Image.open('arial.png')
#下面为增强部分
enh_con = ImageEnhance.Contrast(im)
contrast = 1.5
image_contrasted = enh_con.enhance(contrast)
#image_contrasted.show()

#增强亮度
enh_bri = ImageEnhance.Brightness(image_contrasted)
brightness = 1.5
image_brightened = enh_bri.enhance(brightness)
#image_brightened.show()
#增强对比度
enh_col = ImageEnhance.Color(image_brightened)
color = 1.5
image_colored = enh_col.enhance(color)
#image_colored.show()
#增强锐度
enh_sha = ImageEnhance.Sharpness(image_colored)
sharpness = 3.0
image_sharped = enh_sha.enhance(sharpness)
#image_sharped.show()

#灰度处理部分
im2=image_sharped.convert("L")
im2.show()
text=pytesseract.image_to_string(im2,lang='chi_sim').strip() #使用image_to_string识别验证码
print(text)

输入的图片:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
输出的结果:
在这里插入图片描述(正确)
在这里插入图片描述(错误)
(成功)
在这里插入图片描述(成功)
结论:
上面的代码能给很好的识别英文和数字,缺陷是代码还不能识别中文,识别英文也可能会出现错误。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值