[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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM