在前面的幾篇文章中,咱們介紹了EasyPR中車牌定位模塊的相關內容。本文開始分析車牌定位模塊後續步驟的車牌判斷模塊。車牌判斷模塊是EasyPR中的基於機器學習模型的一個模塊,這個模型就是做者前文中從機器學習談起中提到的SVM(支持向量機)。
咱們已經知道,車牌定位模塊的輸出是一些候選車牌的圖片。但如何從這些候選車牌圖片中甄選出真正的車牌,就是經過SVM模型判斷/預測獲得的。css
圖1 從候選車牌中選出真正的車牌html
簡單來講,EasyPR的車牌判斷模塊就是將候選車牌的圖片一張張地輸入到SVM模型中,而後問它,這是車牌麼?若是SVM模型回答不是,那麼就繼續下一張,若是是,則把圖片放到一個輸出列表裏。最後把列表輸入到下一步處理。因爲EasyPR使用的是列表做爲輸出,所以它能夠輸出一副圖片中全部的車牌,不像一些車牌識別程序,只能輸出一個車牌結果。算法
圖2 EasyPR輸出多個車牌數據庫
如今,讓咱們一步步地,進入這個SVM模型的核心看看,它是如何作到判斷一副圖片是車牌仍是不是車牌的?本文主要分爲三個大的部分:機器學習
人類是如何判斷一個張圖片所表達的信息呢?簡單來講,人類在成長過程當中,大腦記憶了無數的圖像,而且依次給這些圖像打上了標籤,例如太陽,天空,房子,車子等等。大家還記得當年上幼兒園時的那些教科書麼,上面一個太陽,下面是文字。圖像的組成事實上就是許多個像素,由像素組成的這些信息被輸入大腦中,而後得出這個是什麼東西的回答。咱們在SVM模型中一開始輸入的原始信息也是圖像的全部像素,而後SVM模型經過對這些像素進行分析,輸出這個圖片是不是車牌的結論。模塊化
圖3 經過圖像來學習
函數
SVM模型處理的是最簡單的狀況,它只要回答是或者不是這個「二值」問題,比從許多類中檢索要簡單不少。性能
咱們能夠看一下SVM進行判斷的代碼:學習
int CPlateJudge::plateJudge(const vector<Mat>& inVec, vector<Mat>& resultVec) { int num = inVec.size(); for (int j = 0; j < num; j++) { Mat inMat = inVec[j]; Mat p = histeq(inMat).reshape(1, 1); p.convertTo(p, CV_32FC1); int response = (int)svm.predict(p); if (response == 1) { resultVec.push_back(inMat); } } return 0; }
首先咱們讀取這幅圖片,而後把這幅圖片轉爲OPENCV須要的格式;
Mat p = histeq(inMat).reshape(1, 1); p.convertTo(p, CV_32FC1);
接着調用svm的方法predict;
int response = (int)svm.predict(p);
perdict方法返回的值是1的話,就表明是車牌,不然就不是;
if (response == 1) { resultVec.push_back(inMat); }
svm是類CvSVM的一個對象。這個類是opencv裏內置的一個機器學習類。
CvSVM svm;
opencv的CvSVM的實現基於libsvm(具體信息能夠看opencv的官方文檔的介紹 )。
libsvm是臺灣大學林智仁(Lin Chih-Jen)教授寫的一個世界知名的svm庫(可能算是目前業界使用率最高的一個庫)。官方主頁地址是這裏。
libsvm的實現基於SVM這個算法,90年代初由Vapnik等人提出。國內幾篇較好的解釋svm原理的博文:cnblog的LeftNotEasy(解釋的易懂),pluskid的博文(專業有配圖)。
做爲支持向量機的發明者,Vapnik是一位機器學習界極爲重要的大牛。最近這位大牛也加入了Facebook。
圖4 SVM之父Vapnik
svm的perdict方法的輸入是待預測數據的特徵,也稱之爲features。在這裏,咱們輸入的特徵是圖像所有的像素。因爲svm要求輸入的特徵應該是一個向量,而Mat是與圖像寬高對應的矩陣,所以在輸入前咱們須要使用reshape(1,1)方法把矩陣拉伸成向量。除了所有像素之外,也能夠有其餘的特徵,具體看第三部分「SVM調優」。
predict方法的輸出是float型的值,咱們須要把它轉變爲int型後再進行判斷。若是是1表明就是車牌,不然不是。這個"1"的取值是由你在訓練時輸入的標籤決定的。標籤,又稱之爲label,表明某個數據的分類。若是你給SVM模型輸入一個車牌,並告訴它,這個圖片的標籤是5。那麼你這邊判斷時所用的值就應該是5。
以上就是svm模型判斷的全過程。事實上,在你使用EasyPR的過程當中,這些所有都是透明的。你不須要轉變圖片格式,也不須要調用svm模型preditct方法,這些所有由EasyPR在內部調用。
那麼,咱們應該作什麼?這裏的關鍵在於CvSVM這個類。我在前面的機器學習論文中介紹過,機器學習過程的步驟就是首先你搜集大量的數據,而後把這些數據輸入模型中訓練,最後再把生成的模型拿出來使用。
訓練和預測兩個過程是分開的。也就是說大家在使用EasyPR時用到的CvSVM類是我在先前就訓練好的。我是如何把我訓練好的模型交給各位使用的呢?CvSVM類有個方法,把訓練好的結果以xml文件的形式存儲,我就是把這個xml文件隨EasyPR發佈,並讓程序在執行前先加載好這個xml。這個xml的位置就是在文件夾Model下面--svm.xml文件。
圖5 model文件夾下的svm.xml
若是看CPlateJudge的代碼,在構造函數中調用了LoadModel()這個方法。
CPlateJudge::CPlateJudge() { //cout << "CPlateJudge" << endl; m_path = "model/svm.xml"; LoadModel(); }
LoadModel()方法的主要任務就是裝載model文件夾下svm.xml這個模型。
void CPlateJudge::LoadModel() { svm.clear(); svm.load(m_path.c_str(), "svm"); }
若是你把這個xml文件換成其餘的,那麼你就能夠改變EasyPR車牌判斷的內核,從而實現你本身的車牌判斷模塊。
後面的部分所有是告訴你如何有效地實現一個本身的模型(也就是svm.xml文件)。若是你對EasyPR的需求僅僅在應用層面,那麼到目前的瞭解就足夠了。若是你但願可以改善EasyPR的效果,定製一個本身的車牌判斷模塊,那麼請繼續往下看。
二.SVM訓練
恭喜你!從如今開始起,你將真正踏入機器學習這個神祕而且充滿未知的領域。至今爲止,機器學習不少方法的背後原理都很是複雜,但衆多的實踐都證實了其有效性。與許多其餘學科不一樣,機器學習界更爲關注的是最終方法的效果,也就是偏重以實踐效果做爲評判標準。所以很是適合從工程的角度入手,經過本身動手實踐一個項目裏來學習,而後再轉入理論。這個過程已經被證實是有效的,本文的做者在開發EasyPR的時候,尚未任何機器學習的理論基礎。後來的知識是將經過學習相關課程後獲取的。
簡而言之,SVM訓練部分的目標就是經過一批數據,而後生成一個表明咱們模型的xml文件。
EasyPR中全部關於訓練的方法均可以在svm_train.cpp中找到(1.0版位於train/code文件夾下,1.1版位於src/train文件夾下)。
一個訓練過程包含5個步驟,見下圖:
圖6 一個完整的SVM訓練流程
下面具體講解一下這5個步驟,步驟後面的括號裏表明的是這個步驟主要的輸入與輸出。
1. preprocss(原始數據->學習數據(未標籤))
預處理步驟主要處理的是原始數據到學習數據的轉換過程。原始數據(raw data),表示你一開始拿到的數據。這些數據的狀況是取決你具體的環境的,可能有各類問題。學習數據(learn data),是能夠被輸入到模型的數據。
爲了可以進入模型訓練,必須將原始數據處理爲學習數據,同時也可能進行了數據的篩選。比方說你有10000張原始圖片,出於性能考慮,你只想用1000張圖片訓練,那麼你的預處理過程就是將這10000張處理爲符合訓練要求的1000張。你生成的1000張圖片中應該包含兩類數據:真正的車牌圖片和不是車牌的圖片。若是你想讓你的模型可以區分這兩種類型。你就必須給它輸入這兩類的數據。
經過EasyPR的車牌定位模塊PlateLocate能夠生成大量的候選車牌圖片,裏面包括模型須要的車牌和非車牌圖片。但這些候選車牌是沒有通過分類的,也就是說沒有標籤。下步工做就是給這些數據貼上標籤。
2. label (學習數據(未標籤)->學習數據)
訓練過程的第二步就是將未貼標籤的數據轉化爲貼過標籤的學習數據。咱們所要作的工做只是將車牌圖片放到一個文件夾裏,非車牌圖片放到另外一個文件夾裏。在EasyPR裏,這兩個文件夾分別叫作HasPlate和NoPlate。若是你打開train/data/plate_detect_svm後,你就會看到這兩個壓縮包,解壓後就是打好標籤的數據(1.1版本在同層learn data文件夾下面)。
若是有人問我開發一個機器學習系統最耗時的步驟是哪一個,我會絕不猶豫的回答:「貼標籤」。誠然,各位看到的壓縮包裏已經有打好標籤的數據了。但各位可能不知道做者花在貼這些標籤上的時間。粗略估計,整個EasyPR開發過程當中有70%的時間都在貼標籤。SVM模型還好,只有兩個類,訓練數據僅有1000張。到了ANN模型那裏,字符的類數有40多個,並且訓練數據有4000張左右。那時候的貼標籤過程,真是不堪回首的回憶,來回移動文件致使做者手常常性的很是酸。後來我一度想找個實習生幫我作這些工做。但轉念一想,這些苦我都不肯承擔,何苦還要那些小夥子承擔呢。「己所不欲,勿施於人」。算了,既然這是機器學習者的命,那就欣然接受吧。幸虧在這段磨礪的時光,我逐漸掌握了一個方法,大幅度減小了我貼標籤的時間與精力。否則,我可能還未開始寫這個系列的教程,就已經累吐血了。開發EasyPR1.1版本時,新增了一大批數據,所以又有了貼標籤的過程。幸虧使用這個方法,使得相關時間大幅度減小。這個方法叫作逐次迭代自動標籤法。在後面會介紹這個方法。
貼標籤後的車牌數據以下圖:
圖7 在HasPlate文件夾下的圖片
貼標籤後的非車牌數據下圖:
圖8 在NoPlate文件夾下的圖片
擁有了貼好標籤的數據之後,下面的步驟是分組,也稱之爲divide過程。
3. divide (學習數據->分組數據)
分組這個過程是EasyPR1.1版新引入的方法。
在貼完標籤之後,我擁有了車牌圖片和非車牌圖片共幾千張。在我直接訓練前,不急。先拿出30%的數據,只用剩下的70%數據進行SVM模型的訓練,訓練好的模型再用這30%數據進行一個效果測試。這30%數據充當的做用就是一個評判數據測試集,稱之爲test data,另70%數據稱之爲train data。因而一個完整的learn data被分爲了train data和test data。
圖9 數據分組過程
在EasyPR1.0版是沒有test data概念的,全部數據都輸入訓練,而後直接在原始的數據上進行測試。直接在原始的數據集上測試與單獨劃分出30%的數據測試效果究竟有多少不一樣?
事實上,咱們訓練出模型的根本目的是爲了對未知的,新的數據進行預測與判斷。
當使用訓練的數據進行測試時,因爲模型已經考慮到了訓練數據的特徵,所以很難將這個測試效果推廣到其餘未知數據上。若是使用單獨的測試集進行驗證,因爲測試數據集跟模型的生成沒有關聯,所以能夠很好的反映出模型推廣到其餘場景下的效果。這個過程就能夠簡單描述爲你不能夠拿你給學生的複習提綱捲去考學生,而是應該出一份考察知識點同樣,但題目不同的卷子。前者的方式沒法區分出真正學會的人和死記硬背的人,然後者就能有效地反映出哪些人才是真正「學會」的。
在divide的過程當中,注意不管在train data和test data中都要保持數據的標籤,也就是說車牌數據仍然歸到HasPlate文件夾,非車牌數據歸到NoPlate文件夾。因而,車牌圖片30%歸到test data下面的hasplate文件夾,70%歸到train data下面的hasplate文件夾,非車牌圖片30%歸到test data下面的noplate文件夾,70%歸到train data下面的noplate文件夾。因而在文件夾train 和 test下面又有兩個子文件夾,他們的結構樹就是下圖:
圖10 分組後的文件樹
divide數據結束之後,咱們就能夠進入真正的機器學習過程。也就是對數據的訓練過程。
4. train (訓練數據->模型)
模型在代碼裏的表明就是CvSVM類。在這一步中所要作的就是加載train data,而後用CvSVM類的train方法進行訓練。這個步驟只針對的是上步中生成的總數據70%的訓練數據。
具體來講,分爲如下幾個子步驟:
1) 加載待訓練的車牌數據。見下面這段代碼。
void getPlate(Mat& trainingImages, vector<int>& trainingLabels) { char * filePath = "train/data/plate_detect_svm/HasPlate/HasPlate"; vector<string> files; getFiles(filePath, files ); int size = files.size(); if (0 == size) cout << "No File Found in train HasPlate!" << endl; for (int i = 0;i < size;i++) { cout << files[i].c_str() << endl; Mat img = imread(files[i].c_str()); img= img.reshape(1, 1); trainingImages.push_back(img); trainingLabels.push_back(1); } }
注意看,車牌圖像我存儲在的是一個vector<Mat>中,而標籤數據我存儲在的是一個vector<int>中。我將train/HasPlate中的圖像依次取出來,存入vector<Mat>。每存入一個圖像,同時也往vector<int>中存入一個int值1,也就是說圖像和標籤分別存在不一樣的vector對象裏,可是保持一一對應的關係。
2) 加載待訓練的非車牌數據,見下面這段代碼中的函數。基本內容與加載車牌數據相似,不一樣之處在於文件夾是train/NoPlate,而且我往vector<int>中存入的是int值0,表明無車牌。
void getNoPlate(Mat& trainingImages, vector<int>& trainingLabels) { char * filePath = "train/data/plate_detect_svm/NoPlate/NoPlate"; vector<string> files; getFiles(filePath, files ); int size = files.size(); if (0 == size) cout << "No File Found in train NoPlate!" << endl; for (int i = 0;i < size;i++) { cout << files[i].c_str() << endl; Mat img = imread(files[i].c_str()); img= img.reshape(1, 1); trainingImages.push_back(img); trainingLabels.push_back(0); } }
3) 將二者合併。目前擁有了兩個vector<Mat>和兩個vector<int>。將表明車牌圖片和非車牌圖片數據的兩個vector<Mat>組成一個新的Mat--trainingData,而表明車牌圖片與非車牌圖片標籤的兩個vector<int>組成另外一個Mat--classes。接着作一些數據類型的調整,以讓其符合svm訓練函數train的要求。這些作完後,數據的準備工做基本結束,下面就是參數配置的工做。
Mat classes;//(numPlates+numNoPlates, 1, CV_32FC1); Mat trainingData;//(numPlates+numNoPlates, imageWidth*imageHeight, CV_32FC1 ); Mat trainingImages; vector<int> trainingLabels; getPlate(trainingImages, trainingLabels); getNoPlate(trainingImages, trainingLabels); Mat(trainingImages).copyTo(trainingData); trainingData.convertTo(trainingData, CV_32FC1); Mat(trainingLabels).copyTo(classes);
4) 配置SVM模型的訓練參數。SVM模型的訓練須要一個CvSVMParams的對象,這個類是SVM模型中訓練對象的參數的組合,如何給這裏的參數賦值,是頗有講究的一個工做。注意,這裏是SVM訓練的核心內容,也是最能體現一個機器學習專家和新手區別的地方。機器學習最後模型的效果差別有很大因素取決與模型訓練時的參數,尤爲是SVM,有很是多的參數供你配置(見下面的代碼)。參數衆可能是一個問題,更爲顯著的是,機器學習模型中參數的一點微調均可能帶來最終結果的巨大差別。
CvSVMParams SVM_params; SVM_params.svm_type = CvSVM::C_SVC; SVM_params.kernel_type = CvSVM::LINEAR; //CvSVM::LINEAR; SVM_params.degree = 0; SVM_params.gamma = 1; SVM_params.coef0 = 0; SVM_params.C = 1; SVM_params.nu = 0; SVM_params.p = 0; SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01);
opencv官網文檔對CvSVMParams類的各個參數有一個詳細的解釋。若是你上過SVM課程的理論部分,你可能對這些參數的意思能搞的明白。但在這裏,咱們能夠不去管參數的含義,由於咱們有更好的方法去解決這個問題。
圖11 SVM各參數的做用
這個緣由在於:EasyPR1.0使用的是liner核,也稱之爲線型核,所以degree和gamma還有coef0三個參數沒有做用。同時,在這裏SVM模型用做的問題是分類問題,那麼nu和p兩個參數也沒有影響。最後惟一能影響的參數只有Cvalue。到了EasyPR1.1版本之後,默認使用的是RBF核,所以須要調整的參數多了一個gamma。
以上參數的選擇均可以用自動訓練(train_auto)的方法去解決,在下面的SVM調優部分會具體介紹train_auto。
5) 開始訓練。OK!數據載入完畢,參數配置結束,一切準備就緒,下面就是交給opencv的時間。咱們只要將前面的 trainingData,classes,以及CvSVMParams的對象SVM_params交給CvSVM類的train函數就能夠。另外,直接使用CvSVM的構造函數,也能夠完成訓練過程。例以下面這行代碼:
CvSVM svm(trainingData, classes, Mat(), Mat(), SVM_params);
訓練開始後,慢慢等一會。機器學習中數據訓練的計算量每每是很是大的,即使現代計算機也要運行很長時間。具體的時間取決於你訓練的數據量的大小以及模型的複雜度。在個人2.0GHz的機器上,訓練1000條數據的SVM模型的時間大約在1分鐘左右。
訓練完成之後,咱們就能夠用CvSVM類的對象svm去進行預測了。若是咱們僅僅須要這個模型,如今能夠把它存到xml文件裏,留待下次使用:
FileStorage fsTo("train/svm.xml", cv::FileStorage::WRITE); svm.write(*fsTo, "svm");
5. test (測試數據->評判指標)
記得咱們還有30%的測試數據了麼?如今是使用它們的時候了。將這些數據以及它們的標籤加載如內存,這個過程與加載訓練數據的過程是同樣的。接着使用咱們訓練好的SVM模型去判斷這些圖片。
下面的步驟是對咱們的模型作指標評判的過程。首先,測試數據是有標籤的數據,這意味着咱們知道每張圖片是車牌仍是不是車牌。另外,用新生成的svm模型對數據進行判斷,也會生成一個標籤,叫作「預測標籤」。「預測標籤」與「標籤」通常是存在偏差的,這也就是模型的偏差。這種偏差有兩種狀況:1.這副圖片是真的車牌,可是svm模型判斷它是「非車牌」;2.這幅圖片不是車牌,但svm模型判斷它是「車牌」。無疑,這兩種狀況都屬於svm模型判斷失誤的狀況。咱們須要設計出來兩個指標,來分別評測這兩種失誤狀況發生的機率。這兩個指標就是下面要說的「準確率」(precision)和「查全率」(recall)。
準確率是統計在我已經預測爲車牌的圖片中,真正車牌數據所佔的比例。假設咱們用ptrue_rtrue表示預測(p)爲車牌而且實際(r)爲車牌的數量,而用ptrue_rfalse表示實際不爲車牌的數量。
準確率的計算公式是:
圖12 precise 準確率
查全率是統計真正的車牌圖片中,我預測爲車牌的圖片所佔的比例。同上,咱們用ptrue_rtrue表示預測與實際都爲車牌的數量。用pfalse_rtrue表示實際爲車牌,但我預測爲非車牌的數量。
查全率的計算公式是:
圖13 recall 查全率
recall的公式與precision公式惟一的區別在於右下角。precision是ptrue_rfalse,表明預測爲車牌但實際不是的數量;而recall是pfalse_rtrue,表明預測是非車牌但實際上是車牌的數量。
簡單來講,precision指標的指望含義就是要「查的準」,recall的指望含義就是「不要漏」。查全率還有一個翻譯叫作「召回率」。但很明顯,召回這個詞沒有反映出查全率所體現出的不要漏的含義。
值得說明的是,precise和recall這兩個值天然是越高越好。可是若是一個高,一個低的話效果會如何,如何跟兩個都中等的狀況進行比較?爲了可以數字化這種比較。機器學習界又引入了FScore這個數值。當precise和recall二者中任一者較高,而另外一者較低是,FScore都會較低。二者中等的狀況下Fscore表現比一高一低要好。當二者都很高時,FScore會很高。
FScore的計算公式以下圖:
圖14 Fscore計算公式
模型測試以及評價指標是EasyPR1.1中新增的功能。在svm_train.cpp的最下面能夠看到這三個指標的計算過程。
訓練心得
經過以上5個步驟,咱們就完成了模型的準備,訓練,測試的所有過程。下面,說一說過程當中的幾點心得。
1. 完善EasyPR的plateLocate功能
在1.1版本中的EasyPR的車牌定位模塊仍然不夠完善。若是你的全部的圖片符合某種通用的模式,參照前面的車牌定位的幾篇教程,以及使用EasyPR新增的Debug模式,你能夠將EasyPR的plateLocate模塊改造爲適合你的狀況。因而,你就能夠利用EasyPR爲你製造大量的學習數據。經過原始數據的輸入,而後經過plateLocate進行定位,再使用EasyPR已有的車牌判斷模塊進行圖片的分類,因而你就能夠獲得一個基本分好類的學習數據。下面所須要作的就是人工覈對,確認一下,保證每張圖片的標籤是正確的,而後再輸入模型進行訓練。
2. 使用「逐次迭代自動標籤法」。
上面討論的貼標籤方法是在EasyPR已經提供了一個訓練好的模型的狀況下。若是一開始手上任何模型都沒有,該怎麼辦?假設目前手裏有成千上萬個經過定位出來的各類候選車牌,手工一個個貼標籤的話,豈不會讓人累吐血?在前文中說過,我在一開始貼標籤過程當中碰到了這個問題,在不斷被折磨與痛苦中,我發現了一個好方法,大幅度減輕了這整個工做的痛苦性。
固然,這個方法很簡單。我若是說出來你必定也不以爲有什麼奇妙的。可是若是在你準備對1000張圖片進行手工貼標籤時,相信我,使用這個方法會讓你最後的時間節省一半。若是你須要僱10我的來貼標籤的話,那麼用了這個方法,可能你最後一我的都不用僱。
這個方法被我稱爲「逐次迭代自動標籤法」。
方法核心很簡單。就是假設你有3000張未分類的圖片。你從中選出1%,也就是30張出來,手工給它們每一個圖片進行分類工做。好的,現在你有了30張貼好標籤的數據了,下步你把它直接輸入到SVM模型中訓練,得到了一個簡單粗曠的模型。以後,你從圖片集中再取出3%的圖片,也就是90張,而後用剛訓練好的模型對這些圖片進行預測,根據預測結果將它們自動分到hasplate和noplate文件夾下面。分完之後,你到這兩個文件夾下面,看看哪些是預測錯的,把hasplate裏預測錯的移動到noplate裏,反之,把noplate裏預測錯的移動到hasplate裏。
接着,你把一開始手工分類好的那30張圖片,結合調整分類的90張圖片,總共120張圖片再輸入svm模型中進行訓練。因而你得到一個比最開始粗曠模型更精準點的模型。而後,你從3000張圖片中再取出6%的圖片來,用這個模型再對它們進行預測,分類....
以上反覆。你每訓練出一個新模型,用它來預測後面更多的數據,而後自動分類。這樣作最大的好處就是你只須要移動那些被分類錯誤的圖片。其餘的圖片已經被正 確的歸類了。注意,在整個過程當中,你每次只須要對新拿出的數據進行人工確認,由於前面的數據已經分好類了。所以,你最好使用兩個文件夾,一個是已經分好類 的數據,另外一個是自動分類數據,須要手工確認的。這樣二者不容易亂。
每次從未標籤的原始數據庫中取出的數據不要多,最好不要超過上次數據的兩倍。這樣能夠保證你的模型的準確率穩步上升。若是想一口吃個大胖子,例如用30張圖片訓練出的模型,去預測1000張數據,那最後結果跟你手工分類沒有任何區別了。
整個方法的原理很簡單,就是不斷迭代循環細化的思想。跟軟件工程中迭代開發過程有殊途同歸之妙。你只要理解了其原理,很容易就能夠複用在任何其餘機器學習模型的訓練中,從而大幅度(或者部分)減輕機器學習過程當中貼標籤的巨大負擔。
回到一個核心問題,對於開發者而言,什麼樣的方法纔是本身實現一個svm.xml的最好方法。有如下幾種選擇。
1.你使用EasyPR提供的svm.xml,這個方式等同於你沒有訓練,那麼EasyPR識別的效率取決於你的環境與EasyPR的匹配度。運氣好的話,這個效果也會不錯。但若是你的環境下車牌跟EasyPR默認的不同。那麼可能就會有點問題。
2.使用EasyPR提供的訓練數據,例如train/data文件下的數據,這樣生成的效果等同於第一步的,不過你能夠調整參數,試試看模型的表現會不會更好一點。
3.使用本身的數據進行訓練。這個方法的適應性最好。首先你得準備你原始的數據,而且寫一個處理方法,可以將原始數據轉化爲學習數據。下面你調用EasyPR的PlateLocate方法進行處理,將候選車牌圖片從原圖片截取出來。你可使用逐次迭代自動標籤思想,使用EasyPR已有的svm模型對這些候選圖片進行預標籤。而後再進行肉眼確認和手工調整,以生成標準的貼好標籤的數據。後面的步驟就能夠按照分組,訓練,測試等過程順次走下去。若是你使用了EasyPR1.1版本,後面的這幾個過程已經幫你實現好代碼了,你甚至能夠直接在命令行選擇操做。
以上就是SVM模型訓練的部分,經過這個步驟的學習,你知道如何經過已有的數據去訓練出一個本身的模型。下面的部分,是對這個訓練過程的一個思考,討論經過何種方法能夠改善我最後模型的效果。
三.SVM調優
SVM調優部分,是經過對SVM的原理進行了解,並運用機器學習的一些調優策略進行優化的步驟。
在這個部分裏,最好要懂一點機器學習的知識。同時,本部分也會講的儘可能通俗易懂,讓人不會有理解上的負擔。在EasyPR1.0版本中,SVM模型的代碼徹底參考了mastering opencv書裏的實現思路。從1.1版本開始,EasyPR對車牌判斷模塊進行了優化,使得模型最後的效果有了較大的改善。
具體說來,本部分主要包括以下幾個子部分:1.RBF核;2.參數調優;3.特徵提取;4.接口函數;5.自動化。
下面分別對這幾個子部分展開介紹。
1.RBF核
SVM中最關鍵的技巧是核技巧。「核」實際上是一個函數,經過一些轉換規則把低維的數據映射爲高維的數據。在機器學習裏,數據跟向量是等同的意思。例如,一個 [174, 72]表示人的身高與體重的數據就是一個兩維的向量。在這裏,維度表明的是向量的長度。(務必要區分「維度」這個詞在不一樣語境下的含義,有的時候咱們會說向量是一維的,矩陣是二維的,這種說法針對的是數據展開的層次。機器學習裏講的維度表明的是向量的長度,與前者不一樣)
簡單來講,低維空間到高維空間映射帶來的好處就是能夠利用高維空間的線型切割模擬低維空間的非線性分類效果。也就是說,SVM模型其實只能作線型分類,可是在線型分類前,它能夠經過核技巧把數據映射到高維,而後在高維空間進行線型切割。高維空間的線型切割完後在低維空間中最後看到的效果就是劃出了一條複雜的分線型分類界限。從這點來看,SVM並無完成真正的非線性分類,而是經過其它方式達到了相似目的,可謂「曲徑通幽」。
SVM模型總共能夠支持多少種核呢。根據官方文檔,支持的核類型有如下幾種:
liner核和rbf核是全部核中應用最普遍的。
liner核,雖然名稱帶核,但它實際上是無核模型,也就是沒有使用核函數對數據進行轉換。所以,它的分類效果僅僅比邏輯迴歸好一點。在EasyPR1.0版中,咱們的SVM模型應用的是liner核。咱們用的是圖像的所有像素做爲特徵。
rbf核,會將輸入數據的特徵維數進行一個維度轉換,具體會轉換爲多少維?這個等於你輸入的訓練量。假設你有500張圖片,rbf核會把每張圖片的數據轉 換爲500維的。若是你有1000張圖片,rbf核會把每幅圖片的特徵轉到1000維。這麼說來,隨着你輸入訓練數據量的增加,數據的維數越多。更方便在高維空間下的分類效果,所以最後模型效果表現較好。
既然選擇SVM做爲模型,並且SVM中核心的關鍵技巧是核函數,那麼理應使用帶核的函數模型,充分利用數據高維化的好處,利用高維的線型分類帶來低維空間下的非線性分類效果。可是,rbf核的使用是須要條件的。
當你的數據量很大,可是每一個數據量的維度通常時,才適合用rbf核。相反,當你的數據量很少,可是每一個數據量的維數都很大時,適合用線型核。
在EasyPR1.0版中,咱們用的是圖像的所有像素做爲特徵,那麼根據車牌圖像的136×36的大小來看的話,就是4896維的數據,再加上咱們輸入的 是彩色圖像,也就是說有R,G,B三個通道,那麼數量還要乘以3,也就是14688個維度。這是一個很是龐大的數據量,你能夠把每幅圖片的數據理解爲長度 爲14688的向量。這個時候,每一個數據的維度很大,而數據的總數不多,若是用rbf核的話,相反效果反而不如無核。
在EasyPR1.1版本時,輸入訓練的數據有3000張圖片,每一個數據的特徵改用直方統計,共有172個維度。這個場景下,若是用rbf核的話,就會將每一個數據的維度轉化爲與數據總數同樣的數量,也就是3000的維度,能夠充分利用數據高維化後的好處。
所以能夠看出,爲了讓EasyPR新版使用rbf核技巧,咱們給訓練數據作了增長,擴充了兩倍的數據,同時,減少了每一個數據的維度。以此知足了rbf核的使用條件。經過使用rbf核來訓練,充分發揮了非線性模型分類的優點,所以帶來了較好的分類效果。
可是,使用rbf核也有一個問題,那就是參數設置的問題。在rbf訓練的過程當中,參數的選擇會顯著的影響最後rbf核訓練出模型的效果。所以必須對參數進行最優選擇。
2.參數調優
傳統的參數調優方法是人手完成的。機器學習工程師觀察訓練出的模型與參數的對應關係,不斷調整,尋找最優的參數。因爲機器學習工程師大部分時間在調整模型的參數,也有了「機器學習就是調參」這個說法。
幸虧,opencv的svm方法中提供了一個自動訓練的方法。也就是由opencv幫你,不斷改變參數,訓練模型,測試模型,最後選擇模型效果最好的那些參數。整個過程是全自動的,徹底不須要你參與,你只須要輸入你須要調整參數的參數類型,以及每次參數調整的步長便可。
如今有個問題,如何驗證svm參數的效果?你可能會說,使用訓練集之外的那30%測試集啊。但事實上,機器學習模型中專門有一個數據集,是用來驗證參數效果的。也就是交叉驗證集(cross validation set,簡稱validate data) 這個概念。
validate data就是專門從train data中取出一部分數據,用這部分數據來驗證參數調整的效果。比方說如今有70%的訓練數據,從中取出20%的數據,剩下50%數據用來訓練,再用訓練出來的模型在20%數據上進行測試。這20%的數據就叫作validate data。真正拿來訓練的數據僅僅只是50%的數據。
正如上面把數據劃分爲test data和train data的理由同樣。爲了驗證參數在新數據上的推廣性,咱們不能用一個訓練數據集,因此咱們要把訓練數據集再細分爲train data和validate data。在train data上訓練,而後在validate data上測試參數的效果。因此說,在一個更通常的機器學習場景中,機器學習工程師會把數據分爲train data,validate data,以及test data。在train data上訓練模型,用validate data測試參數,最後用test data測試模型和參數的總體表現。
說了這麼多,那麼,你們可能要問,是否是還缺乏一個數據集,須要再劃分出來一個validate data吧。可是答案是No。opencv的train_auto函數幫你完成了全部工做,你只須要告訴它,你須要劃分多少個子分組,以及validate data所佔的比例。而後train_auto函數會自動幫你從你輸入的train data中劃分出一部分的validate data,而後自動測試,選擇表現效果最好的參數。
感謝train_auto函數!既幫咱們劃分了參數驗證的數據集,還幫咱們一步步調整參數,最後選擇效果最好的那個參數,可謂是節省了調優過程當中80%的工做。
train_auto函數的調用代碼以下:
svm.train_auto(trainingData, classes, Mat(), Mat(), SVM_params, 10, CvSVM::get_default_grid(CvSVM::C), CvSVM::get_default_grid(CvSVM::GAMMA), CvSVM::get_default_grid(CvSVM::P), CvSVM::get_default_grid(CvSVM::NU), CvSVM::get_default_grid(CvSVM::COEF), CvSVM::get_default_grid(CvSVM::DEGREE), true);
你惟一須要作的就是泡杯茶,翻翻書,而後慢慢等待這計算機幫你處理好全部事情(時間較長,由於每次調整參數又得從新訓練一次)。做者最近的一次訓練的耗時爲1個半小時)。
訓練完畢後,看看模型和參數在test data上的表現把。99%的precise和98%的recall。很是棒,比任何一次手工配的效果都好。
3.特徵提取
在rbf核介紹時提到過,輸入數據的特徵的維度如今是172,那麼這個數字是如何計算出來的?如今的特徵用的是直方統計函數,也就是先把圖像二值化,而後統計圖像中一行元素中1的數目,因爲輸入圖像有36行,所以有36個值,再統計圖像中每一列中1的數目,圖像有136列,所以有136個值,二者相加正好等於172。新的輸入數據的特徵提取函數就是下面的代碼:
// ! EasyPR的getFeatures回調函數 // !本函數是獲取垂直和水平的直方圖圖值 void getHistogramFeatures(const Mat& image, Mat& features) { Mat grayImage; cvtColor(image, grayImage, CV_RGB2GRAY); Mat img_threshold; threshold(grayImage, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY); features = getTheFeatures(img_threshold); }
咱們輸入數據的特徵再也不是所有的三原色的像素值了,而是抽取過的一些特徵。從原始的圖像到抽取後的特徵的過程就被稱爲特徵提取的過程。在1.0版中沒有特徵提取的概念,是直接把圖像中所有像素做爲特徵的。這要感謝羣裏的「若是有一天」同窗,他堅持認爲所有像素的輸入是最低級的作法,認爲用特徵提取後的效果會好多。我問大概能到多少準確率,當時的準確率有92%,我覺得已經很高了,結果他說能到99%。在半信半疑中我嘗試了,果然如他所說,結合了rbf核與新特徵訓練的模型達到的precise在99%左右,並且recall也有98%,這真是個使人咋舌而且很是驚喜的成績。
「若是有一天」建議用的是SFIT特徵提取或者HOG特徵提取,因爲時間緣由,這二者我沒有實現,可是把函數留在了那裏。留待之後有時間完成。在這個過程當中,我充分體會到了開源的力量,若是不是把軟件開源,若是不是有這麼多優秀的你們一塊兒討論,這樣的思路與改善是不可能出現的。
4.接口函數
因爲有SIFT以及HOG等特徵沒有實現,並且將來有可能會有更多有效的特徵函數出現。所以我把特徵函數抽象爲藉口。使用回調函數的思路實現。全部回調函數的代碼都在feature.cpp中,開發者能夠實現本身的回調函數,並把它賦值給EasyPR中的某個函數指針,從而實現自定義的特徵提取。也許大家會有更多更好的特徵的想法與創意。
關於特徵其實有更多的思考,原始的SVM模型的輸入是圖像的所有像素,正如人類在小時候經過圖像識別各類事物的過程。後來SVM模型的輸入是通過抽取的特 徵。正如隨着人類接觸的事物愈來愈多,會發現單憑圖像愈來愈難區分一些很是類似的東西,因而學會了總結特徵。例如太陽就是圓的,黃色,在天空等,能夠憑藉 這些特徵就進行區分和判斷。
從本質上說,特徵是區分事物的關鍵特性。這些特性,必定是從某些維度去看待的。例如,蘋果和梨子,一個是綠色,一個是黃色,這就是顏色的維度;魚和鳥,一個在水裏,一個在空中,這是位置的區分,也就是空間的維度。特徵,是許多維度中最有區分意義的維度。傳統數據倉庫中的OLAP,也稱爲多維分析,提供了人類從多個維度觀察,比較的能力。經過人類的觀察比較,從多個維度中挑選出來的維度,就是要分析目標的特徵。從這點來看,機器學習與多維分析有了關聯。多維分析提供了選擇特徵的能力。而機器學習能夠根據這些特徵進行建模。
機器學習界也有不少算法,專門是用來從數據中抽取特徵信息的。例如傳統的PCA(主成分分析)算法以及最近流行的深度學習中的 AutoEncoder(自動編碼機)技術。這些算法的主要功能就是在數據中學習出最可以明顯區分數據的特徵,從而提高後續的機器學習分類算法的效果。
說一個特徵學習的案例。做者買車時,常常會把大衆的兩款車--邁騰與帕薩特給弄混,由於二者實在太像了。你們能夠到網上去搜一下這兩車的圖片。若是不依賴後排的文字,光靠外形實在難以將兩車區分開來(雖然從生產商來講,前者是一汽大衆生產的,產地在長春,後者是上海大衆生產的,產地在上海。兩個不一樣的公司,南北兩個地方,相差了十萬八千里)。後來我經過仔細觀察,終於發現了一個明顯區分兩輛車的特徵,後來我再也沒有認錯過。這個特徵就是:邁騰的前臉有四條銀槓,而帕薩特只有三條,邁騰比帕薩特多一條銀槓。能夠這麼說,就是這麼一條銀槓,分割了北和南兩個地方生產的汽車。
圖15 一條銀槓,分割了「北」和「南」
在這裏區分的過程,我是經過不斷學習與研究才發現了這些區分的特徵,這充分說明了事物的特徵也是能夠被學習的。若是讓機器學習中的特徵選擇方法PCA和AutoEncoder來分析的話,按理來講它們也應該找出這條銀槓,不然它們就沒法作到對這兩類最有效的分類與判斷。若是沒有找到的話,證實咱們目前的特徵選擇算法還有不少的改進空間(與這個案例相似的還有大衆的另兩款車,高爾夫和Polo。它們兩的區分也是用一樣的道理。相比邁騰和帕薩特,高爾夫和Polo價格差異的更大,因此區分的特徵也更有價值)。
5.自動化
最後我想簡單談一下EasyPR1.1新增的自動化訓練功能與命令行。你們可能看到第二部分介紹SVM訓練時我將過程分紅了5個步驟。事實上,這些步驟中的不少過程是能夠模塊化的。一開始的時候我寫一些不相關的代碼函數,幫我處理各類須要解決的問題,例如數據的分組,打標籤等等。但後來,我把思路理清後,我以爲這幾個步驟中不少的代碼均可以通用。因而我把一些步驟模塊化出來,造成通用的函數,並寫了一個命令行界面去調用它們。在你運行EasyPR1.1版後,在你看到的第一個命令行界面選擇「3.SVM訓練過程」,你就能夠看到這些所有的命令。
圖16 svm訓練命令行
這裏的命令主要有6個部分。第一個部分是最可能須要修改代碼的地方,由於每一個人的原始數據(raw data)都是不同的,所以你須要在data_prepare.cpp中找到這個函數,改寫成適應你格式的代碼。接下來的第二個部分之後的功能基本均可以複用。例如自動貼標籤(注意貼完之後要人工覈對一下)。
第三個到第六部分功能相似。若是你的數據還沒分組,那麼你執行3之後,系統自動幫你分組,而後訓練,再測試驗證。第四個命令行省略了分組過程。第五個命令行部分省略訓練過程。第六個命令行省略了前面全部過程,只作最後模型的測試部分。
讓咱們回顧一下SVM調優的五個思路。第一部分是rbf核,也就是模型選擇層次,根據你的實際環境選擇最合適的模型。第二部分是參數調優,也就是參數優化層次,這部分的參數最好經過一個驗證集來確認,也可使用opencv自帶的train_auto函數。第三部分是特徵抽取部分,也就是特徵甄選們,要能選擇出最能反映數據本質區別的特徵來。在這方面,pca以及深度學習技術中的autoencoder可能都會有所幫助。第四部分是通用接口部分,爲了給優化留下空間,須要抽象出接口,方便後續的改進與對比。第五部分是自動化部分,爲了節省時間,將大量能夠自動化處理的功能模塊化出來,而後提供一些方便的操做界面。前三部分是從機器學習的效果來提升,後兩部分是從軟件工程的層面去優化。
總結起來,就是模型,參數,特徵,接口,模塊五個層面。經過這五個層面,能夠有效的提升機器學習模型訓練的效果與速度,從而下降機器學習工程實施的難度與提高相關的效率。當須要對機器學習模型進行調優的時候,咱們能夠從這五個層面去考慮。
後記
講到這裏,本次的SVM開發詳解也算是結束了。相信經過這篇文檔,以及做者的一些心得,會對你在SVM模型的開發上面帶來一些幫助。下面的工做能夠考慮把這些相關的方法與思路運用到其餘領域,或着改善EasyPR目前已有的模型與算法。若是你找出了比目前更好實現的思路,而且你願意跟咱們分享,那咱們是很是歡迎的。
EasyPR1.1的版本發生了較大的變化。我爲了讓它擁有多人協做,衆包開發的能力,想過不少辦法。最後決定引入了GDTS(General Data Test Set,通用測試數據集,也就是新的image/general_test下的衆多車牌圖片)以及GDSL(General Data Share License,通用數據分享協議,image/GDSL.txt)。這些概念與協議的引入很是重要,可能會改變目前車牌識別與機器學習在國內學習研究的格局。在下期的EasyPR開發詳解中我會重點介紹1.1版的新加入功能以及這兩個概念和背後的思想,歡迎繼續閱讀。
上一篇仍是第四篇,爲何本期SVM開發詳解屬於EasyPR開發的第六篇?事實上,對於目前的車牌定位模塊咱們團隊以爲還有改進空間,因此第五篇的詳解內容是留給改進後的車牌定位模塊的。若是有車牌定位模塊方面好的建議或者但願加入開源團隊,歡迎跟咱們團隊聯繫(easypr_dev@163.com )。您也能夠爲中國的開源事業作出一份貢獻。
版權說明:
本文中的全部文字,圖片,代碼的版權都是屬於做者和博客園共同全部。歡迎轉載,可是務必註明做者與出處。任何未經容許的剽竊以及爬蟲抓取都屬於侵權,做者和博客園保留全部權利。