2016年11月28日 星期一

機器學習(2)--使用OPENCV SVM實作手寫辨識

 
      這一篇我們要來利用OPENCV 所提供的SVM(支援向量機)來實作手寫辨識。在程式實作中,我稍微改變了OPENCV官版原本的範例程式,除了修正在Python3.5+OPENCV3.x
build code 會error以外,程式最後並加入自己手畫的數字圖進行預測。可以使用小畫家直接手繪一20x20 pixel 黑底白字數字圖當作自己輸入預測的樣本。如下<圖一>

另外可以跟前一篇KNN做比較
機器學習(1)--使用OPENCV KNN實作手寫辨識


<圖一>上方為用小畫家手寫輸入的樣本圖片,下方是使用OPENCV SVM辨識出的結果






SVM原理

1.線性資料分割
        如下圖所示,其中含有兩類資料,紅的和藍的。如果是使用 KNN,對於一個測試資料我們要測量它到每一個樣本的距離,從而根據最近鄰居分類。測量所有的距離需要足夠的時間,並且需要大量的記憶體存儲訓練樣本,但是這樣需要佔用很多資源及時間。

<圖二>



        考慮另外一個想法。試著找到一條直線,f(x) = ax1 + bx2 + c, 它可以將所有的資料分割到兩個區域。當我們拿到一個測試資料 X 時,我們只需要把它代入 f(x)。如果 |f(X)| > 0,它就屬於藍色組,否則就屬於紅色組。
         我們把這條線稱為決定邊界(Decision_Boundary)。簡單而且記憶體使用效率也很高。
這種使用一條直線(或者是在高維空間的超平面)將上述資料分成兩組的方法稱為線性分割。從上<圖二>中我們看到有很多條直線可以將資料分為藍紅兩組,那麼,那一條直線是最好的呢?
直覺上這條直線應該是與兩組資料的距離越遠越好。因為測試資料可能有雜訊影響。
這些資料不應該影響分類的準確性。所以這條距離越遠的直線抗雜訊能力也就最強。
因此SVM 要做就是找到一條直線,並使這條直線到<訓練樣本>各組資料的<最短距離>最大。
如下<圖三> 中加粗的直線經過中心。

<圖三>

        要找到決定邊界,就需要使用訓練資料。但我們不需要所有的訓練資料,只需要那些靠近邊界的資料,如上<圖三>中一個藍色的圓圈和兩個紅色的方塊。我們叫他們<支持向量>,經過他們的直線叫做支援平面。
        有了這些資料就足以找到<決定邊界>了。因此我們不須考慮所有的資料,這樣可以減少資料的使用量。
        首先我們找到了分別代表兩組資料的超平面。例如,藍色資料可以用 ωT x + b0 > 1 表示,而紅色資料可以用 ωT x + b0 < −1 表示。ω 叫做權重向量(ω = [ω1, ω2, . . . , ω3]),
x 為特徵向量(x = [x1, x2, . . . , xn])。b0 為 bias(截距)。
       權重向量決定了決定邊界的走向,而 bias 點決定了它的位置。
<決定邊界>被定義為這兩個超平面的中間線(或平面),運算式為 ωT x+b0 = 0。
從支持向量到決定邊界的最短距離為



邊緣長度(Margin)為這個距離的兩倍,我們需要使這個邊緣長度最大。因此我們創建一個新的函數 L(ω,b0) 並使它的值最小:



非線性資料分割
        想像一下,如果一組資料不能被一條直線分為兩組怎麼辦?例如,在一維 空間中"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 = (p1, p2) 和 q = (q1, q2)。用 Ø 表示映射函數,它可以按如下方式將二維的點映射到三維空間中:


 我們要定義一個核函數 K (p, q),它可以用來計算兩個點的內積,如下所示:



        這說明三維空間中的內積可以通過計算二維空間中內積的平方來獲得。這可以擴展到更高維的空間。所以根據低維的資料來計算它們的高維特徵。在進行完映射後,我們就得到了一個高維空間資料。

除了上面的這些概念之外,還有一個問題需要解決,那就是分類錯誤。僅找到具有最大邊緣的決定邊界是不夠的。我們還需要考慮錯誤分類帶來的誤差。有時我們找到的決定邊界的邊緣可能不是最大的但是錯誤分類是最少的。 所以我們需要對我們的模型進行修正來找到一個更好的決定邊界:<最大的邊緣,最小的錯誤分類>。評判標準就被修改為:

         下<圖四>顯示這個概念。對於訓練資料的每一個樣本又增加了一個參數 ξi。它表示訓練樣本到他們所屬類(實際所屬類)的超平面的距離。對於那些分類正確的樣本這個參數自然為0,因為它們會落在它們的支持平面上,但是對於分類錯誤的樣本,這參數不為0。

<圖四>

所以現在新的最優化問題就變成了:


        參數 C 的取值應該如何選擇呢?很明顯應該取決於你的訓練資料。雖然沒有一個統一的答案,但是在選取 C 的值時我們還是應該考慮一下下面的規則:

• 如果 C 的取值比較大,錯誤分類會減少,但是邊緣也會減小。其實就是錯誤分類的代價比較        高,懲罰比較大。(在資料雜訊很小時我們可以考慮選取較大的C值)
• 如果 C 的取值比較小,邊緣會比較大,但錯誤分類的數量會升高。其實就是錯誤分類的代價        比較低,懲罰很小。(如果資料雜訊比較大時,應該考慮較的小C值)


使用OPENCV SVM 進行手寫辨識

    由於OPENCV3.0之後對於ML函式庫的支援不完整,原因是有些機器學習的演算法有專利版權並非全部面費開放,所以要另外再安裝OPENCV 的contrib,這是可以給教育免費所使用的套件。也因此OPENCV3.x之後所引用ML的演算法跟之前OPENCV2.x的版本有些不同。
    對於使用windows環境的可以直接從下列網址:下載已build 好的OPENCV_contrib直接用
pip install安裝。
並需要注意你所安裝的Python版本對應以及X86 或X64版本。
下載網址:
http://www.lfd.uci.edu/~gohlke/pythonlibs/#scipy
例如:我是使用Python 3.5.2 win32 版本,就可以下載以下版本。
opencv_python-3.1.0+contrib_opencl-cp35-cp35m-win32.whl
另外一種安裝方式或者是使用Linux環境,可以到下列網址下載source code並自行編譯使用
https://github.com/opencv/opencv_contrib


手寫辨識實作範例:
 
        接下來就來實作這個範例程式,OPENCV source 裡有附一張1000x2000 pixel的手寫數字圖片,我們把它切成一小塊20x20 pixel的小圖就代表著每一個手寫數字。切割完後可以得到5000筆這樣的小圖當訓練及測試樣本,各占一半。作法會先把圖片灰值化後再把像素展平成一列20x20=400像素,這些2500張訓練樣本的400個的灰值像素即當成特徵值進行SVM訓練,之後再利用另一半測試樣本進行預測,可以得出準確率93.12% ,如下圖。
      在估算完準確率之後,我再進行自己用小畫家產生手寫的圖片進行辨識,結果如下在數字6,8辨識錯誤,可以不斷用小畫家重寫產生新的手寫圖片,再進行辨識看結果會不會不同?動手試看看吧,很有意思。


<圖五>上方為輸入的手寫樣本圖片,下方是使用OPENCV SVM辨識出的結果




<圖六>跟KNN 方式比較,但這並不代表SVM就絕對比KNN好,仍需視所要做的Data                             分類而定,去選擇適合的方法。



<完整範例程式>

import cv2
import numpy as np
from matplotlib import pyplot as plt
#from sklearn import svm, datasets
SZ=20
bin_n = 16 # Number of bins

svm_params = dict( kernel_type = cv2.ml.SVM_LINEAR,
     svm_type = cv2.ml.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')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#print("gray shape=",gray.shape)
# Now we split the image to 5000 cells, each 20x20 size from 1000x2000[rowsxcols] pixel
#先將gray 1000x2000 [rowsxcols] pixel ,row=1000/50 =50個20pixel 
#再將產生的row 的cols =2000/100 =100個20pixel 
cells = [np.hsplit(row,100) for row in np.vsplit(gray,50)] 
#所以cells 為一50x100 的list 每一物件為20x20 pixel

# First half is trainData, remaining is testData
train_cells = [ i[:50] for i in cells ]  #i=0-49 =50個  total =50x50 =2500 個20x20pixel
test_cells = [ i[50:] for i in cells]    #i=0-49 =50個 total =50x50 =2500 個20x20pixel
#so now train_cells and test_cells =50x50[20x20] list
#transfer to [2500][20x20] list
train = list(sum(train_cells, []))
test = list(sum(test_cells, []))

######    Now training   ########################
deskewed=[0]*2500
hogdata=[0]*2500
for i in range(2500):  
 deskewed[i]=deskew(train[i])
 hogdata[i] =hog(deskewed[i])

traindata= np.array(hogdata).astype(np.float32)
#can not use np.float64 ,must use np.int32
responses = np.int32(np.repeat(np.arange(10),250)[:,np.newaxis])

##=====SVM=====================
#SVM in OpenCV 3.1.0 for Python
# Train the SVM
SVM = cv2.ml.SVM_create()
SVM.setType(cv2.ml.SVM_C_SVC)
SVM.setKernel(cv2.ml.SVM_LINEAR)
#SVM.setDegree(0.0)
#SVM.setGamma(0.0)
#SVM.setCoef0(0.0)
#SVM.setC(0)
#SVM.setNu(0.0)
#SVM.setP(0.0)
#SVM.setClassWeights(None)
SVM.setTermCriteria((cv2.TERM_CRITERIA_COUNT, 100, 1.e-06))
SVM.train(traindata, cv2.ml.ROW_SAMPLE, responses)
#predict
#output = SVM.predict(samples)[1].ravel()
#SVM.save('svm_data.dat')
#SVM.save("svm_data.xml")
#SVM.load("svm_data.xml")
#SVM = cv2.ml.SVM_load('svm_data.xml')
######    Now testing  ########################
#get test data
deskewed=[0]*2500
hogdata=[0]*2500
for i in range(2500):  
 deskewed[i]=deskew(test[i])
 hogdata[i] =hog(deskewed[i])
#========================
testData = np.float32(hogdata).reshape(-1,bin_n*4)
result = SVM.predict(testData)

#######   Check Accuracy   ########################
#if result[1].astype(np.int32)==responses 就是match
mask = result[1].astype(np.int32)==responses
correct = np.count_nonzero(mask)
print("correct =",correct)
print (correct*100.0/len(result[1]))

###### Predict testing 2 ########################
#input image data  20x20 pixel
Input_Numer=[0]*10
img_num =[0]*10
deskewed_r =[0]*10
hogdata_r =[0]*10
img_res =[0]*10
testData_r=[0]*10
result=[0]*10
result_str=[0]*10
Input_Numer[0]="0.jpg"
Input_Numer[1]="1.jpg"
Input_Numer[2]="2.jpg"
Input_Numer[3]="3.jpg"
Input_Numer[4]="4.jpg"
Input_Numer[5]="5.jpg"
Input_Numer[6]="6.jpg"
Input_Numer[7]="7.jpg"
Input_Numer[8]="8.jpg"
Input_Numer[9]="9.jpg"
font = cv2.FONT_HERSHEY_SIMPLEX
for i in range(10):  #input 10 number
 img_num[i] = cv2.imread(Input_Numer[i],0)
 deskewed_r[i]=deskew(img_num[i])
 hogdata_r[i] =hog(deskewed_r[i])
 #white screen
 img_res[i] = np.zeros((64,64,3), np.uint8)
 img_res[i][:,:]=[255,255,255]
 #==predict==
 testData_r[i] = np.float32(hogdata_r[i]).reshape(-1,bin_n*4)
 result[i] = SVM.predict(testData_r[i])
 
 print("result[1][0][0] =",result[i][1][0][0].astype(np.int32)) #change type from float32 to int32
 result_str[i]=str(result[i][1][0][0].astype(np.int32))
 if result[i][1][0][0].astype(np.int32)==i:
  cv2.putText(img_res[i],result_str[i],(15,52), font, 2,(0,255,0),3,cv2.LINE_AA)
 else:
  cv2.putText(img_res[i],result_str[i],(15,52), font, 2,(255,0,0),3,cv2.LINE_AA)
#===drawing result======
Input_Numer_name = ['Input 0', 'Input 1','Input 2', 'Input 3','Input 4',\
     'Input 5','Input 6', 'Input 7','Input8', 'Input9']
     
     
predict_Numer_name =['predict 0', 'predict 1','predict 2', 'predict 3','predict 4', \
     'predict 5','predict6 ', 'predict 7','predict 8', 'predict 9']
    
for i in range(10):
 plt.subplot(2,10,i+1),plt.imshow(img_num[i],cmap = 'gray')
 plt.title(Input_Numer_name[i]), plt.xticks([]), plt.yticks([])
 plt.subplot(2,10,i+11),plt.imshow(img_res[i],cmap = 'gray')
 plt.title(predict_Numer_name[i]), plt.xticks([]), plt.yticks([])

plt.show()





歡迎加入FB,AI人工智慧與機器人社團一起討論,
https://www.facebook.com/groups/1852135541678378/2068782386680358/?notif_t=like&notif_id=1477094593187641

加入阿布拉機的3D列印與機器人的FB專頁
https://www.facebook.com/arbu00/


<其他有關OPENCV 文章>
OPENCV(10)--Canny Edge Detection(Canny邊緣檢測)
OPENCV(9)--Image Gradients(圖像梯度)
OPENCV(8)--Histogram & Histograms Equalization(長條圖與長條圖均衡化)
OPENCV(7)--2D Convolution ,Image Filtering and Blurring (旋積,濾波與模糊)
OPENCV(6)--Trackbar(軌道桿)
OPENCV(5)--Drawing
OPENCV(4)--Grayscale,Binarization,Threshole(灰階化,二值化,閥值)
OPENCV(3)--Matplotlib pyplot bassic function
OPENCV(2)--Capture Video from Camera
OPENCV(1 )--How to install OPENCV in Python

以上原理分析及圖片,公式引用底下OPENCV官網
<參考資料:>OPENCV官網
http://docs.opencv.org/3.1.0/index.html