1 // This is mul/clsfy/tests/test_binary_hyperplane.cxx
2 // Copyright: (C) 2000 British Telecommunications PLC
3 #include <iostream>
4 #include <iomanip>
5 #include <ios>
6 #include <string>
7 #include <algorithm>
8 #include <cmath>
9 #include "testlib/testlib_test.h"
10 //:
11 // \file
12 // \brief Tests the clsfy_binary_pdf_classifier class
13 // \author Ian Scott
14 // Test construction, IO etc.
15 
16 #ifdef _MSC_VER
17 #  include "vcl_msvc_warnings.h"
18 #endif
19 #include <clsfy/clsfy_rbf_parzen.h>
20 #include <clsfy/clsfy_k_nearest_neighbour.h>
21 #include <clsfy/clsfy_binary_hyperplane.h>
22 #include <clsfy/clsfy_binary_hyperplane_ls_builder.h>
23 #include <clsfy/clsfy_binary_hyperplane_gmrho_builder.h>
24 #include <vpdfl/vpdfl_axis_gaussian.h>
25 #include <vpdfl/vpdfl_axis_gaussian_sampler.h>
26 #include "vnl/vnl_random.h"
27 #include "vnl/vnl_math.h"
28 #include "vsl/vsl_binary_loader.h"
29 #include "vsl/vsl_vector_io.h"
30 #include <mbl/mbl_data_array_wrapper.h>
31 #include "vpl/vpl.h" // vpl_unlink()
32 
33 #ifndef LEAVE_FILES_BEHIND
34 #define LEAVE_FILES_BEHIND 0
35 #endif
36 
37 void   test_clsfy_geman_mcclure_build();
38 //: Tests the clsfy_binary_hyperplane and clsfy_binary_hyperplane_builder classes
test_binary_hyperplane()39 void test_binary_hyperplane()
40 {
41   std::cout << "*****************************\n"
42            << " Testing clsfy_binary_linear\n"
43            << "*****************************\n";
44 
45   std::vector<vpdfl_axis_gaussian_sampler *> generator(2);//
46   constexpr unsigned nDims = 2;
47   vnl_vector<double> mean0(nDims), var0(nDims), mean1(nDims), var1(nDims);
48   vpdfl_axis_gaussian PDF0, PDF1;
49 
50   mean0.fill(-1.0); mean0(1) = -0.5;
51   mean1.fill(1.0);
52   var0.fill(0.6); var0(0) = 0.8;
53   var1.fill(0.1); var1(0) = 0.8;
54   PDF0.set(mean0, var0);
55   PDF1.set(mean1, var1);
56 
57   generator[0] = (vpdfl_axis_gaussian_sampler *)PDF0.new_sampler();
58   generator[1] = (vpdfl_axis_gaussian_sampler *)PDF1.new_sampler();
59   vnl_random rng;
60   rng.reseed(111333);
61 
62   constexpr unsigned nSamples = 50;
63   constexpr unsigned nTestSamples = 501;
64   std::vector<vnl_vector<double> > trainingVectors(nSamples);
65   std::vector<vnl_vector<double> > testVectors(nTestSamples);
66   std::vector<unsigned> labels(nSamples);
67   std::vector<unsigned> testLabels(nTestSamples);
68   vnl_vector<double> s;
69 
70   std::cout << "Generating test data\n";
71   for (unsigned int i=0; i<nSamples; i++)
72   {
73     int c = rng.lrand32(0,1);
74     labels[i] = c;
75     generator[c]->sample(s);
76     trainingVectors[i] = s;
77   }
78   for (unsigned i=0; i<nTestSamples; i++)
79   {
80     int c = rng.lrand32(0, 1);
81     testLabels[i] = c;
82     generator[c]->sample(s);
83     testVectors[i] = s;
84   }
85   delete generator[0];
86   delete generator[1];
87 
88   std::cout << "****************The Training set****************\n";
89 
90   vnl_vector<double> x(nDims);
91   std::vector<double> out(1);
92   x.fill(0.0);
93   std::cout << "x(2) varies across from -2 to + 2\n"
94            << "x(1) varies down from    2 to  -2\n";
95 
96 #if 0
97 
98   clsfy_k_nearest_neighbour knn;
99   knn.set(trainingVectors, labels);
100   knn.set_k(3);
101   std::cout << "\nKNN output\n" << std::setprecision(4);
102   for (x(0) = 2; x(0) >= -2 ; x(0) -= 0.25)
103   {
104     for (x(1) = -2; x(1) <= 2 ; x(1) += 0.25)
105     {
106       knn.class_probabilities(out, x);
107       std::cout << std::fixed << std::setw(3) << out[0] << ' ';
108     }
109     std::cout << std::endl;
110   }
111 
112   for (x(0) = 2; x(0) >= -2; x(0) -= 0.25)
113   {
114     for (x(1) = -2; x(1) <= 2 ; x(1) += 0.25)
115     {
116       std::cout << knn.classify(x);
117     }
118     std::cout << std::endl;
119   }
120 
121   clsfy_rbf_parzen win;
122   win.set(trainingVectors, labels);
123   win.set_rbf_width(0.2);
124   win.set_power(10);
125   std::cout << "x(2) varies across from -2 to +2\n"
126            << "x(1) varies down from    2 to -2\n\n"
127            << "Training data distribution\n"
128            << std::setprecision(1);
129   for (x(0) = 2; x(0) >= -2 ; x(0) -= 0.25)
130   {
131     for (x(1) = -2; x(1) <= 2 ; x(1) += 0.25)
132     {
133       double v = win.weightings(x);
134       if (v < 0.05)
135         std::cout << "  .  ";
136       else
137         std::cout << std::fixed << std::setw(4) << v << ' ';
138     }
139     std::cout << std::endl;
140   }
141 #endif
142   std::cout<<"======== TESTING BUILDING ===========\n" << std::setprecision(6);
143 
144   clsfy_binary_hyperplane_ls_builder builder;
145 
146   auto *pClassifier =
147     (clsfy_binary_hyperplane*) builder.new_classifier();
148   std::cout << "Finding Least Squares Separator using least squares\n";
149   mbl_data_array_wrapper<vnl_vector<double> > training_set(trainingVectors);
150   std::cout << "Error on Training set ";
151   double train_error = builder.build(*pClassifier, training_set, labels);
152   std::cout << train_error << std::endl;
153   TEST_NEAR("Train error on classifier is good enough", train_error, 0.0, 0.05);
154 
155   std::cout << "****************Testing over test set**************\n";
156 
157   mbl_data_array_wrapper<vnl_vector<double> > test_set_inputs(testVectors);
158 
159   double test_error = clsfy_test_error(*pClassifier, test_set_inputs, testLabels);
160   std::cout << "Error on Testing set " << test_error << std::endl;
161 
162   TEST_NEAR("Test error on classifier is good enough", test_error, 0.0, 0.1);
163 
164   // print input, print output
165 
166   x.fill(0.0);
167   std::cout << "x(2) varies across from -2 to +2\n"
168            << "x(1) varies down from    2 to -2\n" << std::setprecision(4);
169   for (x(0) = 2; x(0) >= -2 ; x(0) -= 0.25)
170   {
171     for (x(1) = -2; x(1) <= 2 ; x(1) += 0.25)
172     {
173       pClassifier->class_probabilities(out, x);
174       std::cout << out[0] << ' ';
175     }
176     std::cout << std::endl;
177   }
178 
179   for (x(0) = 2; x(0) >= -2 ; x(0) -= 0.25)
180   {
181     for (x(1) = -2; x(1) <= 2 ; x(1) += 0.25)
182     {
183       std::cout << pClassifier->classify(x) << ' ';
184     }
185     std::cout << std::endl;
186   }
187 
188   std::cout<<"======== TESTING I/O ===========\n";
189 
190   std::string test_path = "test_binary_hyperplane.bvl.tmp";
191 
192   vsl_b_ofstream bfs_out(test_path);
193   TEST(("Opened " + test_path + " for writing").c_str(), (!bfs_out ), false);
194   vsl_b_write(bfs_out, *pClassifier);
195   bfs_out.close();
196 
197   clsfy_binary_hyperplane classifier_in;
198 
199   vsl_b_ifstream bfs_in(test_path);
200   TEST(("Opened " + test_path + " for reading").c_str(), (!bfs_in ), false);
201   vsl_b_read(bfs_in, classifier_in);
202 
203   bfs_in.close();
204 #if !LEAVE_FILES_BEHIND
205   vpl_unlink(test_path.c_str());
206 #endif
207 
208   std::cout<<"Saved : " << *pClassifier << std::endl
209           <<"Loaded: " << classifier_in << std::endl;
210 
211   TEST("saved classifier = loaded classifier",
212        pClassifier->weights() == classifier_in.weights() &&
213        pClassifier->bias() == classifier_in.bias(),
214        true);
215 
216   TEST_NEAR("saved classifier(x) = loaded classifier(x)",
217             pClassifier->log_l(vnl_vector<double>(nDims, 0.25)),
218             classifier_in.log_l(vnl_vector<double>(nDims, 0.25)), 1.0e-10);
219 
220   std::cout << std::setprecision(6) << std::resetiosflags(std::ios::floatfield);
221 
222   delete pClassifier;
223 
224   test_clsfy_geman_mcclure_build();
225 }
226 
test_clsfy_geman_mcclure_build()227 void test_clsfy_geman_mcclure_build()
228 {
229     std::vector<vpdfl_axis_gaussian_sampler *> generator(2);//
230     constexpr unsigned nDims = 2;
231     vnl_vector<double> meanPos(nDims), varPos(nDims), meanNeg(nDims), varNeg(nDims),origin(nDims);
232     double d=1.0/vnl_math::sqrt2;
233     origin.fill(0.0);
234     std::vector<vnl_vector<double> > basisPos(2);
235     basisPos[0].set_size(2);basisPos[0][0] = -d;basisPos[0][1] = d;
236     basisPos[1].set_size(2);basisPos[1][0] = d;basisPos[1][1] = d;
237 
238     std::vector<vnl_vector<double> > basisNeg(2);
239     double dx =  std::cos(0.96), dy =  std::sin(0.96); //(0.96 is a bit more than 45 degrees in radians)
240     basisNeg[0].set_size(2);basisNeg[0][0] = -dx;basisNeg[0][1] = dy;
241     basisNeg[1].set_size(2);basisNeg[1][0] = dx;basisNeg[1][1] = dy;
242 
243     vpdfl_axis_gaussian PDF0, PDF1;
244 
245     meanPos.fill(0.0);
246     meanPos[0] = -2.75;   meanPos[1] = 2.75;
247     meanNeg.fill(0.0);
248 
249     vnl_vector<double > deltaMeans = meanPos - meanNeg;
250     varNeg[0]= 0.75; varNeg[1] = 3.0;
251     varPos[0] = 1.0; varPos[1] = 4.0;
252     PDF0.set(origin, varNeg);
253     PDF1.set(origin, varPos);
254 
255     generator[0] = (vpdfl_axis_gaussian_sampler *)PDF0.new_sampler();
256     generator[1] = (vpdfl_axis_gaussian_sampler *)PDF1.new_sampler();
257     vnl_random rng;
258     rng.reseed(111333);
259 
260     constexpr unsigned nSamples = 200;
261     constexpr unsigned nTestSamples = 1000;
262     std::vector<vnl_vector<double> > trainingVectors(nSamples);
263     std::vector<vnl_vector<double> > testVectors(nTestSamples);
264     std::vector<unsigned> labels(nSamples);
265     std::vector<unsigned> testLabels(nTestSamples);
266     vnl_vector<double> sbase;
267     vnl_vector<double> s;
268 
269     std::cout << "Generating test data\n";
270     unsigned nall = nSamples+nTestSamples;
271     for (unsigned int i=0; i<nall; i++)
272     {
273         unsigned indicator=0;
274         bool outlier=false;
275         if (i%3==0)
276         {
277             indicator=1;
278         }
279         if (i<nSamples)
280         {
281             if ( indicator)
282             {
283                 if (i%24==0)
284                 {
285                     outlier = true;
286                 }
287             }
288             else if (i%20 == 0)
289             {
290                 outlier = true;
291             }
292         }
293         generator[indicator]->sample(sbase);
294         std::vector<vnl_vector<double> >& basis = (indicator ? basisPos : basisNeg);
295         vnl_vector<double>& mean=(indicator ? meanPos : meanNeg);
296         s = sbase[0]*basis[0] + sbase[1]*basis[1];
297         if (outlier)
298         {
299             if (indicator)
300             {
301                 s += 4.0*deltaMeans; //Shift miles away from the boundary
302             }
303             else
304             {
305                 s -= 2.0*deltaMeans;   // Shift inwards away from the boundary
306             }
307         }
308         if (i<nSamples)
309         {
310             trainingVectors[i] = mean + s;
311             labels[i] = indicator;
312         }
313         else
314         {
315             unsigned j = i-nSamples;
316             testLabels[j] = indicator;
317             testVectors[j] = mean + s;
318         }
319     }
320 
321     delete generator[0];
322     delete generator[1];
323 
324     std::cout<<"======== TESTING BUILDING ===========\n" << std::setprecision(6);
325 
326     //First do an ordinary least squares build for comparison
327     clsfy_binary_hyperplane_ls_builder builder;
328 
329     auto *pClassifier =
330         (clsfy_binary_hyperplane*) builder.new_classifier();
331     std::cout << "Finding Least Squares Separator using least squares\n";
332     mbl_data_array_wrapper<vnl_vector<double> > training_set(trainingVectors);
333     std::cout << "Error on Training set ";
334     double train_errorLS = builder.build(*pClassifier, training_set, labels);
335     std::cout << train_errorLS << std::endl;
336     TEST_NEAR("Train error on classifier is good enough", train_errorLS, 0.0, 0.15);
337     vnl_vector<double> weightsLS = pClassifier->weights();
338     std::cout << "****************Testing over test set**************\n";
339 
340     mbl_data_array_wrapper<vnl_vector<double> > test_set_inputs(testVectors);
341 
342     double test_errorLS = clsfy_test_error(*pClassifier, test_set_inputs, testLabels);
343     std::cout << "Error on Testing set " << test_errorLS << std::endl;
344 
345     TEST_NEAR("Test error on classifier is good enough", test_errorLS, 0.0, 0.25);
346 
347     //Now run the gmrho build but with sigma forced large so that the GM function approximates
348     // to ordinary least square
349     clsfy_binary_hyperplane_gmrho_builder builderGM;
350     std::cout<<std::endl<<"Now training using Geman-McClure builder"<<std::endl
351             << "Error on Training set ";
352     builderGM.set_auto_estimate_sigma(false);
353     builderGM.set_sigma_preset(10.0);
354 
355     #if 0 // commented out
356     {
357         using namespace clsfy_binary_hyperplane_gmrho_builder_helpers;
358         //Now copy from the urggghh data wrapper into a sensible data structure (matrix!)
359         training_set.reset();
360         unsigned num_vars_ = training_set.current().size();
361         unsigned num_examples_ =  training_set.size();
362         vnl_matrix<double> data(num_examples_,num_vars_,0.0);
363         unsigned i=0;
364         do
365         {
366             double* row=data[i++];
367             std::copy(training_set.current().begin(),training_set.current().end(),row);
368         } while (training_set.next());
369 
370         Set up category regression values determined by output class
371             vnl_vector<double> y(num_examples_,0.0);
372         std::transform(labels.begin(),labels.end(),
373                       y.begin(),
374                       category_value(std::count(labels.begin(),labels.end(),1u),labels.size()));
375 
376         vnl_vector<double> weights(weightsLS);
377 
378         weights.set_size(num_vars_+1);
379 
380         weights.update(weightsLS,0);
381         weights[num_vars_] = pClassifier->bias();
382         gmrho_sum costFn(data,y,1.5);
383         double epsilon=0.001;
384         vnl_vector<double> delta(num_vars_+1,epsilon);
385         vnl_vector<double> weights0(weights);
386         weights0+= 2.0*delta;
387         double f0 = costFn.f(weights0);
388         vnl_vector<double > gradf0(num_vars_+1,0.0);
389         vnl_vector<double > gradf1(num_vars_+1,0.0);
390         epsilon=1.0E-6;
391         for (unsigned i=0; i<weights.size();i++)
392         {
393             weights = weights0;
394             weights[i] += epsilon;
395             double f1 = costFn.f(weights);
396             weights = weights0;
397             weights[i] -= epsilon;
398             double f2 = costFn.f(weights);
399             gradf0[i] = (f1 - f2)/(2.0*epsilon);
400         }
401         costFn.gradf(weights0,gradf1);
402         vnl_vector<double> dg = gradf1-gradf0;
403         TEST_NEAR("GM cost function gradient is consistent", dg.inf_norm(), 0.0, 1.0E-4);
404     }
405     #endif // 0
406 
407     clsfy_builder_base* pBase = &builderGM;
408     double train_error = pBase->build(*pClassifier, training_set, 1,labels);
409     std::cout << train_error << std::endl;
410     TEST_NEAR("Train error on classifier is good enough", train_error, 0.0, 0.15);
411     vnl_vector<double > weights = pClassifier->weights();
412     weights.normalize();
413     weightsLS.normalize();
414     vnl_vector<double> diff=weightsLS - weights;
415     TEST_NEAR("Hyperplane close to LS Build at high sigma", diff.inf_norm(), 0.0, 0.02);
416     std::cout << "****************Testing over test set**************\n";
417 
418     double test_error = clsfy_test_error(*pClassifier, test_set_inputs, testLabels);
419     std::cout << "Error on Testing set " << test_error << std::endl;
420     TEST_NEAR("Test error on classifier is good enough", test_error, 0.0, 0.2);
421     double dtest=std::fabs(test_error - test_errorLS);
422     TEST_NEAR("Test error is similar to LS build at high sigma", dtest, 0.0, 0.005);
423 
424     builderGM.set_auto_estimate_sigma(true);
425     builderGM.set_sigma_preset(1.0);
426 
427     std::cout<<"Now doing Geman McClure build with automatic sigma scaling..."<<std::endl;
428 
429     double train_error2 = pBase->build(*pClassifier, training_set,1, labels);
430     std::cout << train_error2 << std::endl;
431     TEST_NEAR("Train error on classifier is good enough", train_error2, 0.0, 0.05);
432     weights = pClassifier->weights();
433     weights.normalize();
434     diff=weightsLS - weights;
435     TEST_NEAR("Hyperplane still fairly close to LS Build", diff.inf_norm(), 0.0, 0.25);
436     std::cout << "****************Testing over test set**************\n";
437 
438     double test_error2 = clsfy_test_error(*pClassifier, test_set_inputs, testLabels);
439     std::cout << "Error on Testing set " << test_error2 << std::endl;
440     TEST("Test error on classifier has improved", test_error2<test_error, true);
441     TEST_NEAR("Test error on classifier is good enough", test_error2, 0.0, 0.10);
442 
443     delete pClassifier;
444 }
445 
446 TESTMAIN(test_binary_hyperplane);
447