[OpenCV-Python] OpenCV 中机器学习 部分 VIII

部分 VIII
机器学习

OpenCV-Python 中文教程(搬运)目录


46 K 近邻(k-Nearest Neighbour )


46.1 理解 K 近邻
目标
  • 本节我们要理解 k 近邻(kNN)的基本概念。
原理
  kNN 可以说是最简单的监督学习分类器了。想法也很简单,就是找出测试数据在特征空间中的最近邻居。我们将使用下面的图片介绍它。

    Understanding kNN
上图中的对象可以分成两组,蓝色方块和红色三角。每一组也可以称为一个 类。我们可以把所有的这些对象看成是一个城镇中房子,而所有的房子分别属于蓝色和红色家族,而这个城镇就是所谓的特征空间。(你可以把一个特征空间看成是所有点的投影所在的空间。例如在一个 2D 的坐标空间中,每个数据都两个特征 x 坐标和 y 坐标,你可以在 2D 坐标空间中表示这些数据。如果每个数据都有 3 个特征呢,我们就需要一个 3D 空间。N 个特征就需要 N 维空间,这个 N 维空间就是特征空间。在上图中,我们可以认为是具有两个特征色2D 空间)。
现在城镇中来了一个新人,他的新房子用绿色圆盘表示。我们要根据他房子的位置把他归为蓝色家族或红色家族。我们把这过程成为 分类。我们应该怎么做呢?因为我们正在学习看 kNN,那我们就使用一下这个算法吧。
一个方法就是查看他最近的邻居属于那个家族,从图像中我们知道最近的是红色三角家族。所以他被分到红色家族。这种方法被称为简单 近邻,因为分类仅仅决定与它最近的邻居。
但是这里还有一个问题。红色三角可能是最近的,但如果他周围还有很多蓝色方块怎么办呢?此时蓝色方块对局部的影响应该大于红色三角。所以仅仅检测最近的一个邻居是不足的。所以我们检测 k 个最近邻居。谁在这 k 个邻居中占据多数,那新的成员就属于谁那一类。如果 k 等于 3,也就是在上面图像中检测 3 个最近的邻居。他有两个红的和一个蓝的邻居,所以他还是属于红色家族。但是如果 k 等于 7 呢?他有 5 个蓝色和 2 个红色邻居,现在他就会被分到蓝色家族了。k 的取值对结果影响非常大。更有趣的是,如果 k 等于 4呢?两个红两个蓝。这是一个死结。所以 k 的取值最好为奇数。这中根据 k 个最近邻居进行分类的方法被称为 kNN。
在 kNN 中我们考虑了 k 个最近邻居,但是我们给了这些邻居相等的权重,这样做公平吗?以 k 等于 4 为例,我们说她是一个死结。但是两个红色三角比两个蓝色方块距离新成员更近一些。所以他更应该被分为红色家族。那用数学应该如何表示呢?我们要根据每个房子与新房子的距离对每个房子赋予不同的权重。距离近的具有更高的权重,距离远的权重更低。然后我们根据两个家族的权重和来判断新房子的归属,谁的权重大就属于谁。这被称为 修改过的kNN。
那这里面些是重要的呢?
• 我们需要整个城镇中每个房子的信息。因为我们要测量新来者到所有现存房子的距离,并在其中找到最近的。如果那里有很多房子,就要占用很大的内存和更多的计算时间。
• 训练和处理几乎不需要时间。
现在我们看看 OpenCV 中的 kNN。


46.1.1 OpenCV 中的 kNN
  我们这里来举一个简单的例子,和上面一样有两个类。下一节我们会有一个更好的例子。
这里我们将红色家族标记为 Class-0,蓝色家族标记为 Class-1。还要再创建 25 个训练数据,把它们非别标记为 Class-0 或者 Class-1。Numpy中随机数产生器可以帮助我们完成这个任务。
然后借助 Matplotlib 将这些点绘制出来。红色家族显示为红色三角蓝色家族显示为蓝色方块。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Feature set containing (x,y) values of 25 known/training data
trainData = np.random.randint(0,100,(25,2)).astype(np.float32)

# Labels each one either Red or Blue with numbers 0 and 1
responses = np.random.randint(0,2,(25,1)).astype(np.float32)

# Take Red families and plot them
red = trainData[responses.ravel()==0]
plt.scatter(red[:,0],red[:,1],80,'r','^')

# Take Blue families and plot them
blue = trainData[responses.ravel()==1]
plt.scatter(blue[:,0],blue[:,1],80,'b','s')

plt.show()

你可能会得到一个与上面类似的图形,但不会完全一样,因为你使用了随机数产生器,每次你运行代码都会得到不同的结果。
下面就是 kNN 算法分类器的初始化,我们要传入一个训练数据集,以及与训练数据对应的分类来训练 kNN 分类器(构建搜索树)。
最后要使用 OpenCV 中的 kNN 分类器,我们给它一个测试数据,让它来进行分类。在使用 kNN 之前,我们应该对测试数据有所了解。我们的数据应该是大小为数据数目乘以特征数目的浮点性数组。然后我们就可以通过计算找到测试数据最近的邻居了。我们可以设置返回的最近邻居的数目。返回值包括:
  1. 由 kNN 算法计算得到的测试数据的类别标志(0 或 1)。如果你想使用最近邻算法,只需要将 k 设置为 1,k 就是最近邻的数目。
  2. k 个最近邻居的类别标志。
  3. 每个最近邻居到测试数据的距离。
让我们看看它是如何工作的。测试数据被标记为绿色。

newcomer = np.random.randint(0,100,(1,2)).astype(np.float32)
plt.scatter(newcomer[:,0],newcomer[:,1],80,'g','o')

knn = cv2.KNearest()
knn.train(trainData,responses)
ret, results, neighbours ,dist = knn.find_nearest(newcomer, 3)

print "result: ", results,"\n"
print "neighbours: ", neighbours,"\n"
print "distance: ", dist

plt.show()

下面是我得到的结果:

result:  [[ 1.]]
neighbours:  [[ 1.  1.  1.]]
distance:  [[ 53.  58.  61.]]

 


这说明我们的测试数据有 3 个邻居,他们都是蓝色,所以它被分为蓝色家族。结果很明显,如下图所示:

    kNN Demo
如果我们有大量的数据要进行测试,可以直接传入一个数组。对应的结果同样也是数组。

# 10 new comers
newcomers = np.random.randint(0,100,(10,2)).astype(np.float32)
ret, results,neighbours,dist = knn.find_nearest(newcomer, 3)
# The results also will contain 10 labels.

 


46.2 使用 kNN 对手写数字 OCR


目标
  • 要根据我们掌握的 kNN 知识创建一个基本的 OCR 程序
  • 使用 OpenCV 自带的手写数字和字母数据测试我们的程序
46.2.1 手写数字的 OCR
  我们的目的是创建一个可以对手写数字进行识别的程序。为了达到这个目的我们需要训练数据和测试数据。OpenCV 安装包中有一副图片(/samples/python2/data/digits.png), 其中有 5000 个手写数字(每个数字重复 500遍)。每个数字是一个 20x20 的小图。所以第一步就是将这个图像分割成 5000个不同的数字。我们在将拆分后的每一个数字的图像重排成一行含有 400 个像素点的新图像。这个就是我们的特征集,所有像素的灰度值。这是我们能创建的最简单的特征集。我们使用每个数字的前 250 个样本做训练数据,剩余的250 个做测试数据。让我们先准备一下:

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('digits.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

# Now we split the image to 5000 cells, each 20x20 size
cells = [np.hsplit(row,100) for row in np.vsplit(gray,50)]

# Make it into a Numpy array. It size will be (50,100,20,20)
x = np.array(cells)

# Now we prepare train_data and test_data.
train = x[:,:50].reshape(-1,400).astype(np.float32) # Size = (2500,400)
test = x[:,50:100].reshape(-1,400).astype(np.float32) # Size = (2500,400)

# Create labels for train and test data
k = np.arange(10)
train_labels = np.repeat(k,250)[:,np.newaxis]
test_labels = train_labels.copy()

# Initiate kNN, train the data, then test it with test data for k=1
knn = cv2.KNearest()
knn.train(train,train_labels)
ret,result,neighbours,dist = knn.find_nearest(test,k=5)

# Now we check the accuracy of classification
# For that, compare the result with test_labels and check which are wrong
matches = result==test_labels
correct = np.count_nonzero(matches)
accuracy = correct*100.0/result.size
print accuracy

现在最基本的 OCR 程序已经准备好了,这个示例中我们得到的准确率为91%。改善准确度的一个办法是提供更多的训练数据,尤其是判断错误的那些数字。为了避免每次运行程序都要准备和训练分类器,我们最好把它保留,这样在下次运行是时,只需要从文件中读取这些数据开始进行分类就可以了。
Numpy 函数 np.savetxt,np.load 等可以帮助我们搞定这些。

# save the data
np.savez('knn_data.npz',train=train, train_labels=train_labels)

# Now load the data
with np.load('knn_data.npz') as data:
    print data.files
    train = data['train']
    train_labels = data['train_labels']

在我的系统中,占用的空间大概为 4.4M。由于我们现在使用灰度值(unint8)作为特征,在保存之前最好先把这些数据装换成 np.uint8 格式,这样就只需要占用 1.1M 的空间。在加载数据时再转会到 float32。


46.2.2 英文字母的 OCR
  接下来我们来做英文字母的 OCR。和上面做法一样,但是数据和特征集有一些不同。现在 OpenCV 给出的不是图片了,而是一个数据文件(/samples/cpp/letter-recognition.data)。如果打开它的话,你会发现它有 20000 行,第一样看上去就像是垃圾。实际上每一行的第一列是我们的一个字母标记。接下来的 16 个数字是它的不同特征。这些特征来源于UCI Machine LearningRepository。你可以在此页找到更多相关信息。
有 20000 个样本可以使用,我们取前 10000 个作为训练样本,剩下的10000 个作为测试样本。我们应在先把字母表转换成 asc 码,因为我们不正直接处理字母。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Load the data, converters convert the letter to a number
data= np.loadtxt('letter-recognition.data', dtype= 'float32', delimiter = ',',
                    converters= {0: lambda ch: ord(ch)-ord('A')})

# split the data to two, 10000 each for train and test
train, test = np.vsplit(data,2)

# split trainData and testData to features and responses
responses, trainData = np.hsplit(train,[1])
labels, testData = np.hsplit(test,[1])

# Initiate the kNN, classify, measure accuracy.
knn = cv2.KNearest()
knn.train(trainData, responses)
ret, result, neighbours, dist = knn.find_nearest(testData, k=5)

correct = np.count_nonzero(result == labels)
accuracy = correct*100.0/10000
print accuracy

准确率达到了 93.22%。同样你可以通过增加训练样本的数量来提高准确率。

 

47 支持向量机


47.1 理解 SVM
目标
  • 对 SVM 有一个直观理解
原理
47.1.1 线性数据分割
  如下图所示,其中含有两类数据,红的和蓝的。如果是使用 kNN,对于一个测试数据我们要测量它到每一个样本的距离,从而根据最近邻居分类。测量所有的距离需要足够的时间,并且需要大量的内存存储训练样本。但是分类下图所示的数据真的需要占用这么多资源吗?

    Test Data
我们在考虑另外一个想法。我们找到了一条直线,f (x) = ax 1 + bx 2 + c,它可以将所有的数据分割到两个区域。当我们拿到一个测试数据 X 时,我们只需要把它代入 f (x)。如果 |f (X)| > 0,它就属于蓝色组,否则就属于红色组。
我们把这条线称为 决定边界(Decision_Boundary)。很简单而且内存使用效率也很高。这种使用一条直线(或者是高位空间种的超平面)上述数据分成两组的方法成为 线性分割。

    Decision Boundary
从上图中我们看到有很多条直线可以将数据分为蓝红两组,那一条直线是最好的呢?直觉上讲这条直线应该是与两组数据的距离越远越好。为什么呢?
因为测试数据可能有噪音影响(真实数据 + 噪声)。这些数据不应该影响分类的准确性。所以这条距离远的直线抗噪声能力也就最强。所以 SVM 要做就是找到一条直线,并使这条直线到(训练样本)各组数据的最短距离最大。下图中加粗的直线经过中心。
要找到决定边界,就需要使用训练数据。我们需要所有的训练数据吗?不,只需要那些靠近边界的数据,如上图中一个蓝色的圆盘和两个红色的方块。我们叫他们 支持向量,经过他们的直线叫做 支持平面。有了这些数据就足以找到决定边界了。我们担心所有的数据。这对于数据简化有帮助。
We need not worry about all the data. It helps in data reduction.
到底发生了什么呢?首先我们找到了分别代表两组数据的超平面。例如,蓝色数据可以用 w^Tx+b_0 > 1 表示,而红色数据可以用 w^Tx+b_0 < -1 表示,ω 叫做 权重向量( w=[w_1, w_2,..., w_n]),x 为 特征向量(x = [x_1,x_2,..., x_n])。b 0 被成为 bias(截距?)。权重向量决定了决定边界的走向,而 bias 点决定了它(决定边界)的位置。决定边界被定义为这两个超平面的中间线(平面),表达式为 w^Tx+b_0 = 0。从支持向量到决定边界的最短距离为 distance_{support \, vectors}=\frac{1}{||w||}
边缘长度为这个距离的两倍,我们需要使这个边缘长度最大。我们要创建一个新的函数 L(w, b_0)并使它的值最小:

      \min_{w, b_0} L(w, b_0) = \frac{1}{2}||w||^2 \; \text{subject to} \; t_i(w^Tx+b_0) \geq 1 \; \forall i
其中 t i 是每一组的标记,t_i \in [-1,1].。


47.1.2 非线性数据分割
  想象一下,如果一组数据不能被一条直线分为两组怎么办?例如,在一维空间中 X 类包含的数据点有(-3,3),O 类包含的数据点有(-1,1)。很明显不可能使用线性分割将 X 和 O 分开。但是有一个方法可以帮我们解决这个问题。使用函数 f(x) = x^2 对这组数据进行映射,得到的 X 为 9,O 为 1,这时就可以使用线性分割了。
或者我们也可以把一维数据转换成两维数据。我们可以使用函数f(x)=(x,x^2)对数据进行映射。这样 X 就变成了(-3,9)和(3,9)而 O 就变成了(-1,1)和(1,1)。同样可以线性分割,简单来说就是在低维空间不能线性分割的数据在高维空间很有可能可以线性分割。
通常我们可以将 d 维数据映射到 D 维数据来检测是否可以线性分割(D>d)。这种想法可以帮助我们通过对低维输入(特征)空间的计算来获得高维空间的点积。我们可以用下面的例子说明。
假设我们有二维空间的两个点:p = (p 1 ,p 2 ) 和 q = (q 1 ,q 2 )。用 Ø 表示映射函数,它可以按如下方式将二维的点映射到三维空间中:

      \phi (p) = (p_{1}^2,p_{2}^2,\sqrt{2} p_1 p_2)
\phi (q) = (q_{1}^2,q_{2}^2,\sqrt{2} q_1 q_2)
我们要定义一个核函数 K (p,q),它可以用来计算两个点的内积,如下所示这说明三维空间中的内积可以通过计算二维空间中内积的平方来获得。这可以扩展到更高维的空间。所以根据低维的数据来计算它们的高维特征。在进行完映射后,我们就得到了一个高维空间数据。

      K(p,q)  = \phi(p).\phi(q) &= \phi(p)^T , \phi(q) \\
                          &= (p_{1}^2,p_{2}^2,\sqrt{2} p_1 p_2).(q_{1}^2,q_{2}^2,\sqrt{2} q_1 q_2) \\
                          &= p_{1}^2 q_{1}^2 + p_{2}^2 q_{2}^2 + 2 p_1 q_1 p_2 q_2 \\
                          &= (p_1 q_1 + p_2 q_2)^2 \\
          \phi(p).\phi(q) &= (p.q)^2
除了上面的这些概念之外,还有一个问题需要解决,那就是分类错误。仅仅找到具有最大边缘的决定边界是不够的。我们还需要考虑错误分类带来的误差。有时我们找到的决定边界的边缘可能不是最大的但是错误分类是最少的。所以我们需要对我们的模型进行修正来找到一个更好的决定边界:最大的边缘,最小的错误分类。评判标准就被修改为:

      min \; ||w||^2 + C(distance \; of \; misclassified \; samples \; to \; their \; correct \; regions)

下图显示这个概念。对于训练数据的每一个样本又增加了一个参数 ξ i 。它表示训练样本到他们所属类(实际所属类)的超平面的距离。对于那些分类正确的样本这个参数为 0,因为它们会落在它们的支持平面上。

    Misclassification
现在新的最优化问题就变成了:

      \min_{w, b_{0}} L(w,b_0) = ||w||^{2} + C \sum_{i} {\xi_{i}} \text{ subject to } y_{i}(w^{T} x_{i} + b_{0}) \geq 1 - \xi_{i} \text{ and } \xi_{i} \geq 0 \text{ } \forall i
参数 C 的取值应该如何选择呢?很明显应该取决于你的训练数据。虽然没有一个统一的答案,但是在选取 C 的取值时我们还是应该考虑一下下面的规则:
  • 如果 C 的取值比较大,错误分类会减少,但是边缘也会减小。其实就是错误分类的代价比较高,惩罚比较大。(在数据噪声很小时我们可以选取较大的 C 值。)
  • 如果 C 的取值比较小,边缘会比较大,但错误分类的数量会升高。其实就是错误分类的代价比较低,惩罚很小。整个优化过程就是为了找到一个具有最大边缘的超平面对数据进行分类。(如果数据噪声比较大时,应该考虑)

47.2 使用 SVM 进行手写数据 OCR
目标
本节我们还是要进行手写数据的 OCR,但这次我们使用的是 SVM 而不是 kNN。


手写数字的 OCR
  在 kNN 中我们直接使用像素的灰度值作为特征向量。这次我们要使用方向梯度直方图Histogram of Oriented Gradients (HOG)作为特征向量。
在计算 HOG 前我们使用图片的二阶矩对其进行抗扭斜(deskew)处理。
所以我们首先要定义一个函数 deskew(),它可以对一个图像进行抗扭斜处理。下面就是 deskew() 函数:

def deskew(img):
    m = cv2.moments(img)
    if abs(m['mu02']) < 1e-2:
        return img.copy()
    skew = m['mu11']/m['mu02']
    M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
    img = cv2.warpAffine(img,M,(SZ, SZ),flags=affine_flags)
    return img

下图显示了对含有数字 0 的图片进行抗扭斜处理后的效果。左侧是原始图像,右侧是处理后的结果。

    Deskew
接下来我们要计算图像的 HOG 描述符,创建一个函数 hog()。为此我们计算图像 X 方向和 Y 方向的 Sobel 导数。然后计算得到每个像素的梯度的方向和大小。把这个梯度转换成 16 位的整数。将图像分为 4 个小的方块,对每一个小方块计算它们的朝向直方图(16 个 bin),使用梯度的大小做权重。这样每一个小方块都会得到一个含有 16 个成员的向量。4 个小方块的 4 个向量就组成了这个图像的特征向量(包含 64 个成员)。这就是我们要训练数据的特征向量。

def hog(img):
    gx = cv2.Sobel(img, cv2.CV_32F, 1, 0)
    gy = cv2.Sobel(img, cv2.CV_32F, 0, 1)
    mag, ang = cv2.cartToPolar(gx, gy)

    # quantizing binvalues in (0...16)
    bins = np.int32(bin_n*ang/(2*np.pi))

    # Divide to 4 sub-squares
    bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
    mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
    hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
    hist = np.hstack(hists)
    return hist

最后,和前面一样,我们将大图分割成小图。使用每个数字的前 250 个作
为训练数据,后 250 个作为测试数据。全部代码如下所示:

import cv2
import numpy as np

SZ=20
bin_n = 16 # Number of bins

svm_params = dict( kernel_type = cv2.SVM_LINEAR,
                    svm_type = cv2.SVM_C_SVC,
                    C=2.67, gamma=5.383 )

affine_flags = cv2.WARP_INVERSE_MAP|cv2.INTER_LINEAR

def deskew(img):
    m = cv2.moments(img)
    if abs(m['mu02']) < 1e-2:
        return img.copy()
    skew = m['mu11']/m['mu02']
    M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
    img = cv2.warpAffine(img,M,(SZ, SZ),flags=affine_flags)
    return img

def hog(img):
    gx = cv2.Sobel(img, cv2.CV_32F, 1, 0)
    gy = cv2.Sobel(img, cv2.CV_32F, 0, 1)
    mag, ang = cv2.cartToPolar(gx, gy)
    bins = np.int32(bin_n*ang/(2*np.pi))    # quantizing binvalues in (0...16)
    bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
    mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
    hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
    hist = np.hstack(hists)     # hist is a 64 bit vector
    return hist

img = cv2.imread('digits.png',0)

cells = [np.hsplit(row,100) for row in np.vsplit(img,50)]

# First half is trainData, remaining is testData
train_cells = [ i[:50] for i in cells ]
test_cells = [ i[50:] for i in cells]

######     Now training      ########################

deskewed = [map(deskew,row) for row in train_cells]
hogdata = [map(hog,row) for row in deskewed]
trainData = np.float32(hogdata).reshape(-1,64)
responses = np.float32(np.repeat(np.arange(10),250)[:,np.newaxis])

svm = cv2.SVM()
svm.train(trainData,responses, params=svm_params)
svm.save('svm_data.dat')

######     Now testing      ########################

deskewed = [map(deskew,row) for row in test_cells]
hogdata = [map(hog,row) for row in deskewed]
testData = np.float32(hogdata).reshape(-1,bin_n*4)
result = svm.predict_all(testData)

#######   Check Accuracy   ########################
mask = result==responses
correct = np.count_nonzero(mask)
print correct*100.0/result.size

准确率达到了 94%。你可以尝试一下不同的参数值,看看能不能达到更高的准确率。或者也可以读一下这个领域的文章并用代码实现它。



48 K 值聚类


48.1 理解 K 值聚类
目标
  • 本节我们要学习 K 值聚类的概念以及它是如何工作的。
原理
  我将用一个最常用的例子来给大家介绍 K 值聚类。


48.1.1 T 恤大小问题
  话说有一个公司要生产一批新的 T 恤。很明显他们要生产不同大小的 T 恤来满足不同顾客的需求。所以这个公司收集了很多人的身高和体重信息,并把这些数据绘制在图上,如下所示:

    T-shirt Problem

肯定不能把每个大小的 T 恤都生产出来,所以他们把所有的人分为三组:小,中,大,这三组要覆盖所有的人。我们可以使用 K 值聚类的方法将所有人分为 3 组,这个算法可以找到一个最好的分法,并能覆盖所有人。如果不能覆盖全部人的话,公司就只能把这些人分为更多的组,可能是 4 个或 5 个甚至更多。如下图:

    People Grouped into Different Sizes

48.1.2 它是如何工作的?
  这个算法是一个迭代过程,我们会借助图片逐步介绍它。
考虑下面这组数据(你也可以把它当成 T 恤问题),我们需要把他们分成两组。

    Test Data
  第一步:随机选取两个重心点,C 1 和 C 2 (有时可以选取数据中的两个点作为起始重心)。
  第二步:计算每个点到这两个重心点的距离,如果距离 C 1 比较近就标记为 0,如果距离 C 2 比较近就标记为 1。(如果有更多的重心点,可以标记为“2”,“3”等)
在我们的例子中我们把属于 0 的标记为红色,属于 1 的标记为蓝色。我们就会得到下面这幅图。
  第三步:重新计算所有蓝色点的重心,和所有红色点的重心,并以这两个点更新重心点的位置。(图片只是为了演示说明而已,并不代表实际数据)重复步骤 2,更新所有的点标记。
我们就会得到下面的图:
    Initial Centroid Selection and Data Collection
继续迭代步骤 2 和 3,直到两个重心点的位置稳定下来。(当然也可以通过设置迭代次数,或者设置重心移动距离的阈值来终止迭代。)。此时这些点到它们相应重心的距离之和最小此时这些点到它们相应重心的距离之和最小。简单来说,C 1 到红色点的距离与 C 2 到蓝色点的距离之和最小。

    minimize \;\bigg[J = \sum_{All\: Red_Points}distance(C1,Red\_Point) + \sum_{All\: Blue\_Points}distance(C2,Blue\_Point)\bigg]
最终结果如下图所示:

    New Centroid Calculated and Data Re-laballed
这就是对 K 值聚类的一个直观解释。要想知道更多细节和数据解释,你应该读一本关于机器学习的教科书或者参考更多资源中的链接。这只是 K 值聚类的基础。现在对这个算法有很多改进,比如:如何选取好的起始重心点,怎样加速迭代过程等。
更多资源
1. Machine Learning Course, Video lectures by Prof. Andrew Ng
(Some of the images are taken from this)



48.2 OpenCV 中的 K 值聚类
目标
  • 学习使用 OpenCV 中的函数 cv2.kmeans() 对数据进行分类


48.2.1 理解函数的参数
输入参数
理解函数的参数
输入参数
  1. samples: 应该是 np.float32 类型的数据,每个特征应该放在一列。
  2. nclusters(K): 聚类的最终数目。
  3. criteria: 终止迭代的条件。当条件满足时,算法的迭代终止。它应该是一个含有 3 个成员的元组,它们是(typw,max_iter,epsilon):
    • type 终止的类型:有如下三种选择:
      – cv2.TERM_CRITERIA_EPS 只有精确度 epsilon 满足是停止迭代。
      – cv2.TERM_CRITERIA_MAX_ITER 当迭代次数超过阈值时停止迭代。
      – cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER上面的任何一个条件满足时停止迭代。
    • max_iter 表示最大迭代次数。
    • epsilon 精确度阈值。
  4. attempts: 使用不同的起始标记来执行算法的次数。算法会返回紧密度最好的标记。紧密度也会作为输出被返回。
  5. flags:用来设置如何选择起始重心。通常我们有两个选择:cv2.KMEANS_PP_CENTERS和 cv2.KMEANS_RANDOM_CENTERS。
输出参数
  1. compactness:紧密度,返回每个点到相应重心的距离的平方和。
  2. labels:标志数组(与上一节提到的代码相同),每个成员被标记为 0,1等
  3. centers:由聚类的中心组成的数组。
现在我们用 3 个例子来演示如何使用 K 值聚类。

48.2.2 仅有一个特征的数据
  假设我们有一组数据,每个数据只有一个特征(1 维)。例如前面的 T 恤问题,我们只使用人们的身高来决定 T 恤的大小。
我们先来产生一些随机数据,并使用 Matplotlib 将它们绘制出来。

import numpy as np
import cv2
from matplotlib import pyplot as plt

x = np.random.randint(25,100,25)
y = np.random.randint(175,255,25)
z = np.hstack((x,y))
z = z.reshape((50,1))
z = np.float32(z)
plt.hist(z,256,[0,256]),plt.show()

现在我们有一个长度为 50,取值范围为 0 到 255 的向量 z。我已经将向量 z 进行了重排,将它变成了一个列向量。当每个数据含有多个特征是这会很有用。然后我们数据类型转换成 np.float32。
我们得到下图:

    Test Data
现在我们使用 KMeans 函数。在这之前我们应该首先设置好终止条件。我的终止条件是:算法执行 10 次迭代或者精确度 epsilon = 1.0。

# Define criteria = ( type, max_iter = 10 , epsilon = 1.0 )
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)

# Set flags (Just to avoid line break in the code)
flags = cv2.KMEANS_RANDOM_CENTERS

# Apply KMeans
compactness,labels,centers = cv2.kmeans(z,2,None,criteria,10,flags)

返回值有紧密度(compactness), 标志和中心。在本例中我的到的中心是 60 和 207。标志的数目与测试数据的多少是相同的,每个数据都会被标记上“0”,“1”等。这取决与它们的中心是什么。现在我们可以根据它们的标志将把数据分两组。

A = z[labels==0]
B = z[labels==1]

 


现在将 A 组数用红色表示,将 B 组数据用蓝色表示,重心用黄色表示。

# Now plot 'A' in red, 'B' in blue, 'centers' in yellow
plt.hist(A,256,[0,256],color = 'r')
plt.hist(B,256,[0,256],color = 'b')
plt.hist(centers,32,[0,256],color = 'y')
plt.show()

下面就是结果:

    Result of KMeans Clustering

含有多个特征的数据
在前面的 T 恤例子中我们只考虑了身高,现在我们也把体重考虑进去,也就是两个特征。
在前一节我们的数据是一个单列向量。每一个特征被排列成一列,每一行对应一个测试样本。
在本例中我们的测试数据适应 50x2 的向量,其中包含 50 个人的身高和体重。第一列对应与身高,第二列对应与体重。第一行包含两个元素,第一个是第一个人的身高,第二个是第一个人的体重。剩下的行对应与其他人的身高和体重。如下图所示:

    Feature Representation
现在我们来编写代码:

import numpy as np
import cv2
from matplotlib import pyplot as plt

X = np.random.randint(25,50,(25,2))
Y = np.random.randint(60,85,(25,2))
Z = np.vstack((X,Y))

# convert to np.float32
Z = np.float32(Z)

# define criteria and apply kmeans()
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
ret,label,center=cv2.kmeans(Z,2,None,criteria,10,cv2.KMEANS_RANDOM_CENTERS)

# Now separate the data, Note the flatten()
A = Z[label.ravel()==0]
B = Z[label.ravel()==1]

# Plot the data
plt.scatter(A[:,0],A[:,1])
plt.scatter(B[:,0],B[:,1],c = 'r')
plt.scatter(center[:,0],center[:,1],s = 80,c = 'y', marker = 's')
plt.xlabel('Height'),plt.ylabel('Weight')
plt.show()

下面是我得到的结果:

    Result of KMeans Clustering

48.2.3 颜色量化
  颜色量化就是减少图片中颜色数目的一个过程。为什么要减少图片中的颜色呢?减少内存消耗!有些设备的资源有限,只能显示很少的颜色。在这种情况下就需要进行颜色量化。我们使用 K 值聚类的方法来进行颜色量化。没有什么新的知识需要介绍了。现在有 3 个特征:R,G,B。所以我们需要把图片数据变形成 Mx3(M 是图片中像素点的数目)的向量。聚类完成后,我们用聚类中心值替换与其同组的像素值,这样结果图片就只含有指定数目的颜色了。下面是代码:

import numpy as np
import cv2

img = cv2.imread('home.jpg')
Z = img.reshape((-1,3))

# convert to np.float32
Z = np.float32(Z)

# define criteria, number of clusters(K) and apply kmeans()
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
K = 8
ret,label,center=cv2.kmeans(Z,K,None,criteria,10,cv2.KMEANS_RANDOM_CENTERS)

# Now convert back into uint8, and make original image
center = np.uint8(center)
res = center[label.flatten()]
res2 = res.reshape((img.shape))

cv2.imshow('res2',res2)
cv2.waitKey(0)
cv2.destroyAllWindows()

下面是 K=8 的结果:

    Color Quantization

posted @ 2018-02-15 12:28 _Undo 阅读(...) 评论(...) 编辑 收藏