1. 程式人生 > >EasyPR--開發詳解(6)SVM開發詳解

EasyPR--開發詳解(6)SVM開發詳解

  在前面的幾篇文章中,我們介紹了EasyPR中車牌定位模組的相關內容。本文開始分析車牌定位模組後續步驟的車牌判斷模組。車牌判斷模組是EasyPR中的基於機器學習模型的一個模組,這個模型就是作者前文中從機器學習談起中提到的SVM(支援向量機)。

  我們已經知道,車牌定位模組的輸出是一些候選車牌的圖片。但如何從這些候選車牌圖片中甄選出真正的車牌,就是通過SVM模型判斷/預測得到的。

   

 


圖1 從候選車牌中選出真正的車牌

  簡單來說,EasyPR的車牌判斷模組就是將候選車牌的圖片一張張地輸入到SVM模型中,然後問它,這是車牌麼?如果SVM模型回答不是,那麼就繼續下一張,如果是,則把圖片放到一個輸出列表裡。最後把列表輸入到下一步處理。由於EasyPR使用的是列表作為輸出,因此它可以輸出一副圖片中所有的車牌,不像一些車牌識別程式,只能輸出一個車牌結果。

 

 圖2 EasyPR輸出多個車牌

  現在,讓我們一步步地,進入這個SVM模型的核心看看,它是如何做到判斷一副圖片是車牌還是不是車牌的?本文主要分為三個大的部分:

  1. SVM應用:描述如何利用SVM模型進行車牌圖片的判斷。
  2. SVM訓練:說明如何通過一系列步驟得到SVM模型。
  3. SVM調優:討論如何對SVM模型進行優化,使其效果更加好。

 一.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; }
View Code

  首先我們讀取這幅圖片,然後把這幅圖片轉為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);
    }
}        
View Code

  注意看,車牌影象我儲存在的是一個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);
    }
}
View Code


  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);
View Code


  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模型總共可以支援多少種核呢。根據官方文件,支援的核型別有以下幾種:

  1. liner核,也就是無核。
  2. rbf核,使用的是高斯函式作為核函式。
  3. poly核,使用多項式函式作為核函式。
  4. sigmoid核,使用sigmoid函式作為核函式。

  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開發的第六篇?事實上,對於目前的車牌定位模組我們團隊覺得還有改進空間,所以第五篇的詳解內容是留給改進後的車牌定位模組的。如果有車牌定位模組方面好的建議或者希望加入開源團隊,歡迎跟我們團隊聯絡([email protected] )。您也可以為中國的開源事業做出一份貢獻。

版權說明:

  本文中的所有文字,圖片,程式碼的版權都是屬於作者和部落格園共同所有。歡迎轉載,但是務必註明作者與出處。任何未經允許的剽竊以及爬蟲抓取都屬於侵權,作者和部落格園保留所有權利。