KNN是最經典的機器學習算法之一。該算法既可以用於數據分類,也可以用於數據回歸預測,其核心思路是在訓練樣本中尋找距離最接近待分類樣本的K個樣本。然後,如果目的是分類,則統計這K個樣本中的各個類別數量,數量最多的類別即認為是待分類樣本的類別;如果目的是回歸預測,則計算這K個樣本的平均值作為預測值。
圖像識別,本質上也是數據分類,也即把每一張圖像歸類,因此KNN算法可以應用於圖像識別。本文首先講解KNN算法的原理,接著講解Opencv中的KNN算法模塊,然後再使用Opencv中的KNN算法模塊對手寫數字圖像進行識別。
KNN算法主要包含以下幾個要素:
1. 訓練樣本。訓練樣本就是預先準備好的數據集,該數據集必須包含所有可能的數據類別,而且數據集中的每個數據都有一個唯一的標籤,用來標識該數據所屬的類別。比如,待分類的圖像有可能屬於的類別包括鳥、狗、馬、飛機、班車這5種類別,給5種類別依次編號0、1、2、3、4,那麼訓練樣本必須包含這5種類別的圖像,且訓練樣本中每一張圖像都有一個範圍在0~4之間的類別標籤。
2. 待分類樣本。也就是待分類的數據,比如一張圖像,把該圖像輸入KNN算法之中,KNN算法對其進行分類,然後輸出類別標籤。
3. 樣本距離。通常每一個樣本都是一維向量的形式(二維、三維、多維數據都可以轉換為一維向量)。衡量一維向量之間的距離,通常有歐式距離,餘弦距離、漢明距離等,其中歐式距離又是最常用的距離度量方法。
假設樣本A和樣本B:
A=[a0 a1 a2 ... an]
B=[b0 b1 b2 ... bn]
那麼樣本A與樣本B的歐式距離為:
4. K值。K值決定尋找訓練樣本中最接近待分類樣本的樣本個數,比如K值取5,那麼對於每一個待分類樣本,都從訓練樣本中尋找5個與其距離最接近的樣本,然後統計這5個樣本中各個類別的數量,數量最多的類別則認為是待分類樣本的類別。K值沒有固定的取值,通常在一開始取5~10,然後多嘗試幾次,根據識別的正確率來調整K值。
舉一個簡單的例子來說明KNN的分類思路,如下圖所示,樣本0為待分類樣本,其可能的分類為矩形、圓形、菱形,取K=6,然後在所有訓練樣本中尋找與樣本0最接近的6個樣本:樣本1、樣本2、樣本3、樣本4、樣本5、樣本6。然後對這6個樣本進行分類統計(每個訓練樣本是有標籤的,所以知道其類別):3個矩形、2個菱形、1個圓形。矩形的數量最多,因此判定樣本0為矩形。
講完原理,下面我們開始講Opencv3.4.1中KNN算法模塊的應用。Opencv3.4.1中已經實現了該算法,並封裝成類,我們只需要調用類的相關接口,並且把輸入參數傳入接口即可得到分類結果。調用接口的步驟如下:
1. 創建KNN類並設置參數。
const int K = 3; cv::Ptr<cv::ml::KNearest> knn = cv::ml::KNearest::create(); knn->setDefaultK(K); knn->setIsClassifier(true); knn->setAlgorithmType(cv::ml::KNearest::BRUTE_FORCE);2. 輸入訓練數據及其標籤:
Mat traindata; //訓練數據的矩陣Mat trainlabel; //訓練數據的標籤
traindata.push_back(srcimage); //srcimage為Mat類型的1行n列的一維矩陣,將該矩陣保存到訓練矩陣中trainlabel.push_back(i); //i為srcimage的標籤,同時將i保存到標籤矩陣中
traindata.convertTo(traindata, CV_32F); //重要:訓練矩陣必須是浮點型數據3. 將訓練數據與標籤輸入KNN模塊中進行訓練:
knn->train(traindata, cv::ml::ROW_SAMPLE, trainlabel);4. 訓練之後,開始對待分類圖像進行分類:
Mat result;//testdata為待分類圖像,被轉換為1行n列的浮點型數據//response為最終得到的分類標籤int response = knn->findNearest(testdata, K, result);下面,我們將Opencv的KNN模塊應用於手寫數字識別。在Opencv3.4.1的samples/data目錄有一張1000*2000的手寫數字圖像,該圖像包含了5000個20*20的小圖像塊,每個小圖像塊就是一個手寫數字,如下圖所示:
首先,我們寫個程序把上圖分割成5000個20*20的圖像塊。把相同的數字保存到同名文件夾中,比如數字0的小圖像塊保存到文件夾0中、數字1的小圖像塊保存到文件夾1中、數字2的小圖像塊保存到文件夾2中。
分割圖像塊的代碼如下:
void read_digital_img(void){ char ad[128] = { 0 }; int filename = 0, filenum = 0; Mat img = imread("digits.png"); Mat gray; cvtColor(img, gray, CV_BGR2GRAY); int b = 20; int m = gray.rows / b; int n = gray.cols / b;
for (int i = 0; i < m; i++) { int offsetRow = i*b; if (i % 5 == 0 && i != 0) { filename++; filenum = 0; }
for (int j = 0; j < n; j++) { int offsetCol = j*b; sprintf_s(ad, "%d/%d.jpg", filename, filenum++); Mat tmp; gray(Range(offsetRow, offsetRow + b), Range(offsetCol, offsetCol + b)).copyTo(tmp); imwrite(ad, tmp); } }}運行上述代碼之後,從0~9的文件夾都保存有對應數字的小圖像塊啦~不同數字的小圖像塊如下圖所示。這時我們有0~9這10個文件夾,每個文件夾都有對應數字的500張小圖像塊,比如文件夾0中有500張不同手寫風格的0數字圖像。
下面我們使用每個文件夾下前400張圖像作為訓練圖像,對KNN模型進行訓練,然後使用該KNN模型對每個文件夾下後100張圖像進行分類,並統計分類結果的準確率。
上代碼:
void KNN_test(void){ char ad[128] = { 0 }; int testnum = 0, truenum = 0; const int K = 3; cv::Ptr<cv::ml::KNearest> knn = cv::ml::KNearest::create(); knn->setDefaultK(K); knn->setIsClassifier(true); knn->setAlgorithmType(cv::ml::KNearest::BRUTE_FORCE);
Mat traindata, trainlabel;
for (int i = 0; i < 10; i++) { for (int j = 0; j < 400; j++) { sprintf_s(ad, "%d/%d.jpg", i, j); Mat srcimage = imread(ad); srcimage = srcimage.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
traindata.convertTo(traindata, CV_32F); knn->train(traindata, cv::ml::ROW_SAMPLE, trainlabel);
for (int i = 0; i < 10; i++) { for (int j = 400; j < 500; j++) { testnum++; sprintf_s(ad, "%d/%d.jpg", i, j); Mat testdata = imread(ad); testdata = testdata.reshape(1, 1); testdata.convertTo(testdata, CV_32F);
Mat result; int response = knn->findNearest(testdata, K, result); if (response == i) { truenum++; } } } cout << "測試總數" << testnum << endl; cout << "正確分類數" << truenum << endl; cout << "準確率:" << (float)truenum / testnum * 100 << "%" << endl;}運行上述代碼,得到結果如下,可以看到,KNN算法對手寫數字圖像的識別(分類)的準確率還是比較高的。
由於手寫數字圖像包含的特徵比較簡單,因此KNN的識別準確率很高。實際上,對於一些複雜的圖像,KNN的識別準確率是很低的。在下一篇文章中,我們將嘗試使用KNN算法來識別更複雜的圖像,並想辦法提高識別的準確率,敬請期待!
尊敬的讀者,您的關注是我繼續更新的強大動力,如果您對我寫的東西感興趣,請長按並識別以下二維碼,關注我的公眾號,多謝多謝!