1 #include "opencv2/core/core.hpp"
2 #include "opencv2/ml/ml.hpp"
3
4 #include <cstdio>
5 #include <vector>
6 #include <iostream>
7
8 using namespace std;
9 using namespace cv;
10 using namespace cv::ml;
11
help()12 static void help()
13 {
14 printf("\nThe sample demonstrates how to train Random Trees classifier\n"
15 "(or Boosting classifier, or MLP, or Knearest, or Nbayes, or Support Vector Machines - see main()) using the provided dataset.\n"
16 "\n"
17 "We use the sample database letter-recognition.data\n"
18 "from UCI Repository, here is the link:\n"
19 "\n"
20 "Newman, D.J. & Hettich, S. & Blake, C.L. & Merz, C.J. (1998).\n"
21 "UCI Repository of machine learning databases\n"
22 "[http://www.ics.uci.edu/~mlearn/MLRepository.html].\n"
23 "Irvine, CA: University of California, Department of Information and Computer Science.\n"
24 "\n"
25 "The dataset consists of 20000 feature vectors along with the\n"
26 "responses - capital latin letters A..Z.\n"
27 "The first 16000 (10000 for boosting)) samples are used for training\n"
28 "and the remaining 4000 (10000 for boosting) - to test the classifier.\n"
29 "======================================================\n");
30 printf("\nThis is letter recognition sample.\n"
31 "The usage: letter_recog [-data <path to letter-recognition.data>] \\\n"
32 " [-save <output XML file for the classifier>] \\\n"
33 " [-load <XML file with the pre-trained classifier>] \\\n"
34 " [-boost|-mlp|-knearest|-nbayes|-svm] # to use boost/mlp/knearest/SVM classifier instead of default Random Trees\n" );
35 }
36
37 // This function reads data and responses from the file <filename>
38 static bool
read_num_class_data(const string & filename,int var_count,Mat * _data,Mat * _responses)39 read_num_class_data( const string& filename, int var_count,
40 Mat* _data, Mat* _responses )
41 {
42 const int M = 1024;
43 char buf[M+2];
44
45 Mat el_ptr(1, var_count, CV_32F);
46 int i;
47 vector<int> responses;
48
49 _data->release();
50 _responses->release();
51
52 FILE* f = fopen( filename.c_str(), "rt" );
53 if( !f )
54 {
55 cout << "Could not read the database " << filename << endl;
56 return false;
57 }
58
59 for(;;)
60 {
61 char* ptr;
62 if( !fgets( buf, M, f ) || !strchr( buf, ',' ) )
63 break;
64 responses.push_back((int)buf[0]);
65 ptr = buf+2;
66 for( i = 0; i < var_count; i++ )
67 {
68 int n = 0;
69 sscanf( ptr, "%f%n", &el_ptr.at<float>(i), &n );
70 ptr += n + 1;
71 }
72 if( i < var_count )
73 break;
74 _data->push_back(el_ptr);
75 }
76 fclose(f);
77 Mat(responses).copyTo(*_responses);
78
79 cout << "The database " << filename << " is loaded.\n";
80
81 return true;
82 }
83
84 template<typename T>
load_classifier(const string & filename_to_load)85 static Ptr<T> load_classifier(const string& filename_to_load)
86 {
87 // load classifier from the specified file
88 Ptr<T> model = StatModel::load<T>( filename_to_load );
89 if( model.empty() )
90 cout << "Could not read the classifier " << filename_to_load << endl;
91 else
92 cout << "The classifier " << filename_to_load << " is loaded.\n";
93
94 return model;
95 }
96
97 static Ptr<TrainData>
prepare_train_data(const Mat & data,const Mat & responses,int ntrain_samples)98 prepare_train_data(const Mat& data, const Mat& responses, int ntrain_samples)
99 {
100 Mat sample_idx = Mat::zeros( 1, data.rows, CV_8U );
101 Mat train_samples = sample_idx.colRange(0, ntrain_samples);
102 train_samples.setTo(Scalar::all(1));
103
104 int nvars = data.cols;
105 Mat var_type( nvars + 1, 1, CV_8U );
106 var_type.setTo(Scalar::all(VAR_ORDERED));
107 var_type.at<uchar>(nvars) = VAR_CATEGORICAL;
108
109 return TrainData::create(data, ROW_SAMPLE, responses,
110 noArray(), sample_idx, noArray(), var_type);
111 }
112
TC(int iters,double eps)113 inline TermCriteria TC(int iters, double eps)
114 {
115 return TermCriteria(TermCriteria::MAX_ITER + (eps > 0 ? TermCriteria::EPS : 0), iters, eps);
116 }
117
test_and_save_classifier(const Ptr<StatModel> & model,const Mat & data,const Mat & responses,int ntrain_samples,int rdelta,const string & filename_to_save)118 static void test_and_save_classifier(const Ptr<StatModel>& model,
119 const Mat& data, const Mat& responses,
120 int ntrain_samples, int rdelta,
121 const string& filename_to_save)
122 {
123 int i, nsamples_all = data.rows;
124 double train_hr = 0, test_hr = 0;
125
126 // compute prediction error on train and test data
127 for( i = 0; i < nsamples_all; i++ )
128 {
129 Mat sample = data.row(i);
130
131 float r = model->predict( sample );
132 r = std::abs(r + rdelta - responses.at<int>(i)) <= FLT_EPSILON ? 1.f : 0.f;
133
134 if( i < ntrain_samples )
135 train_hr += r;
136 else
137 test_hr += r;
138 }
139
140 test_hr /= nsamples_all - ntrain_samples;
141 train_hr = ntrain_samples > 0 ? train_hr/ntrain_samples : 1.;
142
143 printf( "Recognition rate: train = %.1f%%, test = %.1f%%\n",
144 train_hr*100., test_hr*100. );
145
146 if( !filename_to_save.empty() )
147 {
148 model->save( filename_to_save );
149 }
150 }
151
152
153 static bool
build_rtrees_classifier(const string & data_filename,const string & filename_to_save,const string & filename_to_load)154 build_rtrees_classifier( const string& data_filename,
155 const string& filename_to_save,
156 const string& filename_to_load )
157 {
158 Mat data;
159 Mat responses;
160 bool ok = read_num_class_data( data_filename, 16, &data, &responses );
161 if( !ok )
162 return ok;
163
164 Ptr<RTrees> model;
165
166 int nsamples_all = data.rows;
167 int ntrain_samples = (int)(nsamples_all*0.8);
168
169 // Create or load Random Trees classifier
170 if( !filename_to_load.empty() )
171 {
172 model = load_classifier<RTrees>(filename_to_load);
173 if( model.empty() )
174 return false;
175 ntrain_samples = 0;
176 }
177 else
178 {
179 // create classifier by using <data> and <responses>
180 cout << "Training the classifier ...\n";
181 // Params( int maxDepth, int minSampleCount,
182 // double regressionAccuracy, bool useSurrogates,
183 // int maxCategories, const Mat& priors,
184 // bool calcVarImportance, int nactiveVars,
185 // TermCriteria termCrit );
186 Ptr<TrainData> tdata = prepare_train_data(data, responses, ntrain_samples);
187 model = RTrees::create();
188 model->setMaxDepth(10);
189 model->setMinSampleCount(10);
190 model->setRegressionAccuracy(0);
191 model->setUseSurrogates(false);
192 model->setMaxCategories(15);
193 model->setPriors(Mat());
194 model->setCalculateVarImportance(true);
195 model->setActiveVarCount(4);
196 model->setTermCriteria(TC(100,0.01f));
197 model->train(tdata);
198 cout << endl;
199 }
200
201 test_and_save_classifier(model, data, responses, ntrain_samples, 0, filename_to_save);
202 cout << "Number of trees: " << model->getRoots().size() << endl;
203
204 // Print variable importance
205 Mat var_importance = model->getVarImportance();
206 if( !var_importance.empty() )
207 {
208 double rt_imp_sum = sum( var_importance )[0];
209 printf("var#\timportance (in %%):\n");
210 int i, n = (int)var_importance.total();
211 for( i = 0; i < n; i++ )
212 printf( "%-2d\t%-4.1f\n", i, 100.f*var_importance.at<float>(i)/rt_imp_sum);
213 }
214
215 return true;
216 }
217
218
219 static bool
build_boost_classifier(const string & data_filename,const string & filename_to_save,const string & filename_to_load)220 build_boost_classifier( const string& data_filename,
221 const string& filename_to_save,
222 const string& filename_to_load )
223 {
224 const int class_count = 26;
225 Mat data;
226 Mat responses;
227 Mat weak_responses;
228
229 bool ok = read_num_class_data( data_filename, 16, &data, &responses );
230 if( !ok )
231 return ok;
232
233 int i, j, k;
234 Ptr<Boost> model;
235
236 int nsamples_all = data.rows;
237 int ntrain_samples = (int)(nsamples_all*0.5);
238 int var_count = data.cols;
239
240 // Create or load Boosted Tree classifier
241 if( !filename_to_load.empty() )
242 {
243 model = load_classifier<Boost>(filename_to_load);
244 if( model.empty() )
245 return false;
246 ntrain_samples = 0;
247 }
248 else
249 {
250 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
251 //
252 // As currently boosted tree classifier in MLL can only be trained
253 // for 2-class problems, we transform the training database by
254 // "unrolling" each training sample as many times as the number of
255 // classes (26) that we have.
256 //
257 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
258
259 Mat new_data( ntrain_samples*class_count, var_count + 1, CV_32F );
260 Mat new_responses( ntrain_samples*class_count, 1, CV_32S );
261
262 // 1. unroll the database type mask
263 printf( "Unrolling the database...\n");
264 for( i = 0; i < ntrain_samples; i++ )
265 {
266 const float* data_row = data.ptr<float>(i);
267 for( j = 0; j < class_count; j++ )
268 {
269 float* new_data_row = (float*)new_data.ptr<float>(i*class_count+j);
270 memcpy(new_data_row, data_row, var_count*sizeof(data_row[0]));
271 new_data_row[var_count] = (float)j;
272 new_responses.at<int>(i*class_count + j) = responses.at<int>(i) == j+'A';
273 }
274 }
275
276 Mat var_type( 1, var_count + 2, CV_8U );
277 var_type.setTo(Scalar::all(VAR_ORDERED));
278 var_type.at<uchar>(var_count) = var_type.at<uchar>(var_count+1) = VAR_CATEGORICAL;
279
280 Ptr<TrainData> tdata = TrainData::create(new_data, ROW_SAMPLE, new_responses,
281 noArray(), noArray(), noArray(), var_type);
282 vector<double> priors(2);
283 priors[0] = 1;
284 priors[1] = 26;
285
286 cout << "Training the classifier (may take a few minutes)...\n";
287 model = Boost::create();
288 model->setBoostType(Boost::GENTLE);
289 model->setWeakCount(100);
290 model->setWeightTrimRate(0.95);
291 model->setMaxDepth(5);
292 model->setUseSurrogates(false);
293 model->setPriors(Mat(priors));
294 model->train(tdata);
295 cout << endl;
296 }
297
298 Mat temp_sample( 1, var_count + 1, CV_32F );
299 float* tptr = temp_sample.ptr<float>();
300
301 // compute prediction error on train and test data
302 double train_hr = 0, test_hr = 0;
303 for( i = 0; i < nsamples_all; i++ )
304 {
305 int best_class = 0;
306 double max_sum = -DBL_MAX;
307 const float* ptr = data.ptr<float>(i);
308 for( k = 0; k < var_count; k++ )
309 tptr[k] = ptr[k];
310
311 for( j = 0; j < class_count; j++ )
312 {
313 tptr[var_count] = (float)j;
314 float s = model->predict( temp_sample, noArray(), StatModel::RAW_OUTPUT );
315 if( max_sum < s )
316 {
317 max_sum = s;
318 best_class = j + 'A';
319 }
320 }
321
322 double r = std::abs(best_class - responses.at<int>(i)) < FLT_EPSILON ? 1 : 0;
323 if( i < ntrain_samples )
324 train_hr += r;
325 else
326 test_hr += r;
327 }
328
329 test_hr /= nsamples_all-ntrain_samples;
330 train_hr = ntrain_samples > 0 ? train_hr/ntrain_samples : 1.;
331 printf( "Recognition rate: train = %.1f%%, test = %.1f%%\n",
332 train_hr*100., test_hr*100. );
333
334 cout << "Number of trees: " << model->getRoots().size() << endl;
335
336 // Save classifier to file if needed
337 if( !filename_to_save.empty() )
338 model->save( filename_to_save );
339
340 return true;
341 }
342
343
344 static bool
build_mlp_classifier(const string & data_filename,const string & filename_to_save,const string & filename_to_load)345 build_mlp_classifier( const string& data_filename,
346 const string& filename_to_save,
347 const string& filename_to_load )
348 {
349 const int class_count = 26;
350 Mat data;
351 Mat responses;
352
353 bool ok = read_num_class_data( data_filename, 16, &data, &responses );
354 if( !ok )
355 return ok;
356
357 Ptr<ANN_MLP> model;
358
359 int nsamples_all = data.rows;
360 int ntrain_samples = (int)(nsamples_all*0.8);
361
362 // Create or load MLP classifier
363 if( !filename_to_load.empty() )
364 {
365 model = load_classifier<ANN_MLP>(filename_to_load);
366 if( model.empty() )
367 return false;
368 ntrain_samples = 0;
369 }
370 else
371 {
372 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
373 //
374 // MLP does not support categorical variables by explicitly.
375 // So, instead of the output class label, we will use
376 // a binary vector of <class_count> components for training and,
377 // therefore, MLP will give us a vector of "probabilities" at the
378 // prediction stage
379 //
380 // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
381
382 Mat train_data = data.rowRange(0, ntrain_samples);
383 Mat train_responses = Mat::zeros( ntrain_samples, class_count, CV_32F );
384
385 // 1. unroll the responses
386 cout << "Unrolling the responses...\n";
387 for( int i = 0; i < ntrain_samples; i++ )
388 {
389 int cls_label = responses.at<int>(i) - 'A';
390 train_responses.at<float>(i, cls_label) = 1.f;
391 }
392
393 // 2. train classifier
394 int layer_sz[] = { data.cols, 100, 100, class_count };
395 int nlayers = (int)(sizeof(layer_sz)/sizeof(layer_sz[0]));
396 Mat layer_sizes( 1, nlayers, CV_32S, layer_sz );
397
398 #if 1
399 int method = ANN_MLP::BACKPROP;
400 double method_param = 0.001;
401 int max_iter = 300;
402 #else
403 int method = ANN_MLP::RPROP;
404 double method_param = 0.1;
405 int max_iter = 1000;
406 #endif
407
408 Ptr<TrainData> tdata = TrainData::create(train_data, ROW_SAMPLE, train_responses);
409
410 cout << "Training the classifier (may take a few minutes)...\n";
411 model = ANN_MLP::create();
412 model->setLayerSizes(layer_sizes);
413 model->setActivationFunction(ANN_MLP::SIGMOID_SYM, 0, 0);
414 model->setTermCriteria(TC(max_iter,0));
415 model->setTrainMethod(method, method_param);
416 model->train(tdata);
417 cout << endl;
418 }
419
420 test_and_save_classifier(model, data, responses, ntrain_samples, 'A', filename_to_save);
421 return true;
422 }
423
424 static bool
build_knearest_classifier(const string & data_filename,int K)425 build_knearest_classifier( const string& data_filename, int K )
426 {
427 Mat data;
428 Mat responses;
429 bool ok = read_num_class_data( data_filename, 16, &data, &responses );
430 if( !ok )
431 return ok;
432
433
434 int nsamples_all = data.rows;
435 int ntrain_samples = (int)(nsamples_all*0.8);
436
437 // create classifier by using <data> and <responses>
438 cout << "Training the classifier ...\n";
439 Ptr<TrainData> tdata = prepare_train_data(data, responses, ntrain_samples);
440 Ptr<KNearest> model = KNearest::create();
441 model->setDefaultK(K);
442 model->setIsClassifier(true);
443 model->train(tdata);
444 cout << endl;
445
446 test_and_save_classifier(model, data, responses, ntrain_samples, 0, string());
447 return true;
448 }
449
450 static bool
build_nbayes_classifier(const string & data_filename)451 build_nbayes_classifier( const string& data_filename )
452 {
453 Mat data;
454 Mat responses;
455 bool ok = read_num_class_data( data_filename, 16, &data, &responses );
456 if( !ok )
457 return ok;
458
459 Ptr<NormalBayesClassifier> model;
460
461 int nsamples_all = data.rows;
462 int ntrain_samples = (int)(nsamples_all*0.8);
463
464 // create classifier by using <data> and <responses>
465 cout << "Training the classifier ...\n";
466 Ptr<TrainData> tdata = prepare_train_data(data, responses, ntrain_samples);
467 model = NormalBayesClassifier::create();
468 model->train(tdata);
469 cout << endl;
470
471 test_and_save_classifier(model, data, responses, ntrain_samples, 0, string());
472 return true;
473 }
474
475 static bool
build_svm_classifier(const string & data_filename,const string & filename_to_save,const string & filename_to_load)476 build_svm_classifier( const string& data_filename,
477 const string& filename_to_save,
478 const string& filename_to_load )
479 {
480 Mat data;
481 Mat responses;
482 bool ok = read_num_class_data( data_filename, 16, &data, &responses );
483 if( !ok )
484 return ok;
485
486 Ptr<SVM> model;
487
488 int nsamples_all = data.rows;
489 int ntrain_samples = (int)(nsamples_all*0.8);
490
491 // Create or load Random Trees classifier
492 if( !filename_to_load.empty() )
493 {
494 model = load_classifier<SVM>(filename_to_load);
495 if( model.empty() )
496 return false;
497 ntrain_samples = 0;
498 }
499 else
500 {
501 // create classifier by using <data> and <responses>
502 cout << "Training the classifier ...\n";
503 Ptr<TrainData> tdata = prepare_train_data(data, responses, ntrain_samples);
504 model = SVM::create();
505 model->setType(SVM::C_SVC);
506 model->setKernel(SVM::LINEAR);
507 model->setC(1);
508 model->train(tdata);
509 cout << endl;
510 }
511
512 test_and_save_classifier(model, data, responses, ntrain_samples, 0, filename_to_save);
513 return true;
514 }
515
main(int argc,char * argv[])516 int main( int argc, char *argv[] )
517 {
518 string filename_to_save = "";
519 string filename_to_load = "";
520 string data_filename = "../data/letter-recognition.data";
521 int method = 0;
522
523 int i;
524 for( i = 1; i < argc; i++ )
525 {
526 if( strcmp(argv[i],"-data") == 0 ) // flag "-data letter_recognition.xml"
527 {
528 i++;
529 data_filename = argv[i];
530 }
531 else if( strcmp(argv[i],"-save") == 0 ) // flag "-save filename.xml"
532 {
533 i++;
534 filename_to_save = argv[i];
535 }
536 else if( strcmp(argv[i],"-load") == 0) // flag "-load filename.xml"
537 {
538 i++;
539 filename_to_load = argv[i];
540 }
541 else if( strcmp(argv[i],"-boost") == 0)
542 {
543 method = 1;
544 }
545 else if( strcmp(argv[i],"-mlp") == 0 )
546 {
547 method = 2;
548 }
549 else if( strcmp(argv[i], "-knearest") == 0 || strcmp(argv[i], "-knn") == 0 )
550 {
551 method = 3;
552 }
553 else if( strcmp(argv[i], "-nbayes") == 0)
554 {
555 method = 4;
556 }
557 else if( strcmp(argv[i], "-svm") == 0)
558 {
559 method = 5;
560 }
561 else
562 break;
563 }
564
565 if( i < argc ||
566 (method == 0 ?
567 build_rtrees_classifier( data_filename, filename_to_save, filename_to_load ) :
568 method == 1 ?
569 build_boost_classifier( data_filename, filename_to_save, filename_to_load ) :
570 method == 2 ?
571 build_mlp_classifier( data_filename, filename_to_save, filename_to_load ) :
572 method == 3 ?
573 build_knearest_classifier( data_filename, 10 ) :
574 method == 4 ?
575 build_nbayes_classifier( data_filename) :
576 method == 5 ?
577 build_svm_classifier( data_filename, filename_to_save, filename_to_load ):
578 -1) < 0)
579 {
580 help();
581 }
582 return 0;
583 }
584