easypr解析(六)


  恭喜你!从现在开始起,你将真正踏入机器学习这个神秘并且充满未知的领域。至今为止,机器学习很多方法的背后原理都非常复杂,但众多的实践都证明了其有效性。与许多其他学科不同,机器学习界更为关注的是最终方法的效果,也就是偏重以实践效果作为评判标准。因此非常适合从工程的角度入手,通过自己动手实践一个项目里来学习,然后再转入理论。这个过程已经被证明是有效的,本文的作者在开发EasyPR的时候,还没有任何机器学习的理论基础。后来的知识是将通过学习相关课程后获取的。

  简而言之,SVM训练部分的目标就是通过一批数据,然后生成一个代表我们模型的xml文件。
  EasyPR中所有关于训练的方法都可以在svm_train.cpp中找到(1.0版位于train/code文件夹下,1.1版位于src/train文件夹下)。
  一个训练过程包含5个步骤,见下图:

     
下面具体讲解一下这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版本时,新增了一大批数据,因此又有了贴标签的过程。幸好使用这个方法,使得相关时间大幅度减少。这个方法叫做逐次迭代自动标签法。在后面会介绍这个方法。
  贴标签后的车牌数据如下图:
贴标签后的非车牌数据下图:
 拥有了贴好标签的数据以后,下面的步骤是分组,也称之为divide过程。

  3. divide (学习数据->分组数据)
  分组这个过程是EasyPR1.1版新引入的方法。
  在贴完标签以后,我拥有了车牌图片和非车牌图片共几千张。在我直接训练前,不急。先拿出30%的数据,只用剩下的70%数据进行SVM模型的训练,训练好的模型再用这30%数据进行一个效果测试。这30%数据充当的作用就是一个评判数据测试集,称之为test data,另70%数据称之为train data。于是一个完整的learn data被分为了train data和test data。
    
在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下面又有两个子文件夹,他们的结构树就是下图
                                                                    

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课程的理论部分,你可能对这些参数的意思能搞的明白。但在这里,我们可以不去管参数的含义,因为我们有更好的方法去解决这个问题。
 这个原因在于: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)。
 训练心得


  通过以上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模型训练的部分,通过这个步骤的学习,你知道如何通过已有的数据去训练出一个自己的模型。下面的部分,是对这个训练过程的一个思考,讨论通过何种方法可以改善我最后模型的效果。
  
  



  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值