SHOGUN  v2.0.0
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
QPBSVMLib.cpp
Go to the documentation of this file.
1 /*-----------------------------------------------------------------------
2  *
3  * This program is free software; you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation; either version 3 of the License, or
6  * (at your option) any later version.
7  *
8  * Library for solving QP task required for learning SVM without bias term.
9  *
10  * Written (W) 2006-2009 Vojtech Franc, xfrancv@cmp.felk.cvut.cz
11  * Written (W) 2007 Soeren Sonnenburg
12  * Copyright (C) 2006-2009 Center for Machine Perception, CTU FEL Prague
13  * Copyright (C) 2007-2009 Fraunhofer Institute FIRST
14  *
15  *
16  * min 0.5*x'*H*x + f'*x
17  *
18  * subject to C >= x(i) >= 0 for all i
19  *
20  * H [dim x dim] is symmetric positive semi-definite matrix.
21  * f [dim x 1] is an arbitrary vector.
22  *
23  * The precision of found solution is given by parameters
24  * tmax, tolabs, tolrel which define the stopping conditions:
25  *
26  * t >= tmax -> exit_flag = 0 Number of iterations.
27  * UB-LB <= tolabs -> exit_flag = 1 Abs. tolerance.
28  * UB-LB <= UB*tolrel -> exit_flag = 2 Relative tolerance.
29  *
30  * UB ... Upper bound on the optimal solution.
31  * LB ... Lower bound on the optimal solution.
32  * t ... Number of iterations.
33  * History ... Value of LB and UB wrt. number of iterations.
34  *
35  * 1. Generalized Gauss-Seidel methods
36  * exitflag = qpbsvm_sca( &get_col, diag_H, f, UB, dim, tmax,
37  * tolabs, tolrel, x, Nabla, &t, &History, verb )
38  *
39  * 2. Greedy variant - Udpate variable yielding the best improvement.
40  * exitflag = qpbsvm_scas( &get_col, diag_H, f, UB, dim, tmax,
41  * tolabs, tolrel, x, Nabla, &t, &History, verb )
42  *
43  * 3. Updates variable which most violates the KKT conditions
44  * exitflag = qpbsvm_scamv( &get_col, diag_H, f, UB, dim, tmax,
45  * tolabs, tolrel, tolKKT, x, Nabla, &t, &History, verb )
46  *
47 -------------------------------------------------------------------- */
48 
49 #include <math.h>
50 #include <string.h>
51 #include <limits.h>
52 
53 #include <shogun/lib/config.h>
54 #include <shogun/io/SGIO.h>
57 
60 
61 using namespace shogun;
62 
63 #define HISTORY_BUF 1000000
64 
65 #define INDEX(ROW,COL,DIM) ((COL*DIM)+ROW)
66 
68 {
69  SG_UNSTABLE("CQPBSVMLib::CQPBSVMLib()", "\n");
70 
71  m_H=0;
72  m_dim = 0;
73  m_diag_H = NULL;
74 
75  m_f = NULL;
76  m_UB = 0.0;
77  m_tmax = INT_MAX;
78  m_tolabs = 0;
79  m_tolrel = 1e-6;
80  m_tolKKT = 0;
81  m_solver = QPB_SOLVER_SCA;
82 }
83 
85  float64_t* H, int32_t n, float64_t* f, int32_t m, float64_t UB)
86 : CSGObject()
87 {
88  ASSERT(H && n>0);
89  m_H=H;
90  m_dim = n;
91  m_diag_H=NULL;
92 
93  m_f=f;
94  m_UB=UB;
95  m_tmax = INT_MAX;
96  m_tolabs = 0;
97  m_tolrel = 1e-6;
98  m_tolKKT = 0;
99  m_solver = QPB_SOLVER_SCA;
100 }
101 
103 {
104  SG_FREE(m_diag_H);
105 }
106 
107 int32_t CQPBSVMLib::solve_qp(float64_t* result, int32_t len)
108 {
109  int32_t status = -1;
110  ASSERT(len==m_dim);
112  for (int32_t i=0; i<m_dim; i++)
113  Nabla[i]=m_f[i];
114 
115  SG_FREE(m_diag_H);
116  m_diag_H=SG_MALLOC(float64_t, m_dim);
117 
118  for (int32_t i=0; i<m_dim; i++)
119  m_diag_H[i]=m_H[i*m_dim+i];
120 
121  float64_t* History=NULL;
122  int32_t t;
123  int32_t verb=0;
124 
125  switch (m_solver)
126  {
127  case QPB_SOLVER_GRADDESC:
128  status = qpbsvm_gradient_descent(result, Nabla, &t, &History, verb );
129  break;
130  case QPB_SOLVER_GS:
131  status = qpbsvm_gauss_seidel(result, Nabla, &t, &History, verb );
132  break;
133  case QPB_SOLVER_SCA:
134  status = qpbsvm_sca(result, Nabla, &t, &History, verb );
135  break;
136  case QPB_SOLVER_SCAS:
137  status = qpbsvm_scas(result, Nabla, &t, &History, verb );
138  break;
139  case QPB_SOLVER_SCAMV:
140  status = qpbsvm_scamv(result, Nabla, &t, &History, verb );
141  break;
142  case QPB_SOLVER_PRLOQO:
143  status = qpbsvm_prloqo(result, Nabla, &t, &History, verb );
144  break;
145 #ifdef USE_CPLEX
146  case QPB_SOLVER_CPLEX:
147  status = qpbsvm_cplex(result, Nabla, &t, &History, verb );
148 #else
149  SG_ERROR("cplex not enabled at compile time - unknow solver\n");
150 #endif
151  break;
152  default:
153  SG_ERROR("unknown solver\n");
154  break;
155  }
156 
157  SG_FREE(History);
158  SG_FREE(Nabla);
159  SG_FREE(m_diag_H);
160  m_diag_H=NULL;
161 
162  return status;
163 }
164 
165 /* --------------------------------------------------------------
166 
167 Usage: exitflag = qpbsvm_sca(m_UB, m_dim, m_tmax,
168  m_tolabs, m_tolrel, m_tolKKT, x, Nabla, &t, &History, verb )
169 
170 -------------------------------------------------------------- */
172  float64_t *Nabla,
173  int32_t *ptr_t,
174  float64_t **ptr_History,
175  int32_t verb)
176 {
177  float64_t *History;
178  float64_t *col_H;
179  float64_t *tmp_ptr;
180  float64_t x_old;
181  float64_t delta_x;
182  float64_t xHx;
183  float64_t Q_P;
184  float64_t Q_D;
185  float64_t xf;
186  float64_t xi_sum;
187  int32_t History_size;
188  int32_t t;
189  int32_t i, j;
190  int32_t exitflag;
191  int32_t KKTsatisf;
192 
193  /* ------------------------------------------------------------ */
194  /* Initialization */
195  /* ------------------------------------------------------------ */
196 
197  t = 0;
198 
199  History_size = (m_tmax < HISTORY_BUF ) ? m_tmax+1 : HISTORY_BUF;
200  History=SG_MALLOC(float64_t, History_size*2);
201  memset(History, 0, sizeof(float64_t)*History_size*2);
202 
203  /* compute Q_P and Q_D */
204  xHx = 0;
205  xf = 0;
206  xi_sum = 0;
207  for(i = 0; i < m_dim; i++ ) {
208  xHx += x[i]*(Nabla[i] - m_f[i]);
209  xf += x[i]*m_f[i];
210  xi_sum += CMath::max(0.0,-Nabla[i]);
211  }
212 
213  Q_P = 0.5*xHx + xf;
214  Q_D = -0.5*xHx - m_UB*xi_sum;
215  History[INDEX(0,t,2)] = Q_P;
216  History[INDEX(1,t,2)] = Q_D;
217 
218  if( verb > 0 ) {
219  SG_PRINT("%d: Q_P=%m_f, Q_D=%m_f, Q_P-Q_D=%m_f, (Q_P-Q_D)/|Q_P|=%m_f \n",
220  t, Q_P, Q_D, Q_P-Q_D,(Q_P-Q_D)/CMath::abs(Q_P));
221  }
222 
223  exitflag = -1;
224  while( exitflag == -1 )
225  {
226  t++;
227 
228  for(i = 0; i < m_dim; i++ ) {
229  if( m_diag_H[i] > 0 ) {
230  /* variable update */
231  x_old = x[i];
232  x[i] = CMath::min(m_UB,CMath::max(0.0, x[i] - Nabla[i]/m_diag_H[i]));
233 
234  /* update Nabla */
235  delta_x = x[i] - x_old;
236  if( delta_x != 0 ) {
237  col_H = (float64_t*)get_col(i);
238  for(j = 0; j < m_dim; j++ ) {
239  Nabla[j] += col_H[j]*delta_x;
240  }
241  }
242 
243  }
244  }
245 
246  /* compute Q_P and Q_D */
247  xHx = 0;
248  xf = 0;
249  xi_sum = 0;
250  KKTsatisf = 1;
251  for(i = 0; i < m_dim; i++ ) {
252  xHx += x[i]*(Nabla[i] - m_f[i]);
253  xf += x[i]*m_f[i];
254  xi_sum += CMath::max(0.0,-Nabla[i]);
255 
256  if((x[i] > 0 && x[i] < m_UB && CMath::abs(Nabla[i]) > m_tolKKT) ||
257  (x[i] == 0 && Nabla[i] < -m_tolKKT) ||
258  (x[i] == m_UB && Nabla[i] > m_tolKKT)) KKTsatisf = 0;
259  }
260 
261  Q_P = 0.5*xHx + xf;
262  Q_D = -0.5*xHx - m_UB*xi_sum;
263 
264  /* stopping conditions */
265  if(t >= m_tmax) exitflag = 0;
266  else if(Q_P-Q_D <= m_tolabs) exitflag = 1;
267  else if(Q_P-Q_D <= CMath::abs(Q_P)*m_tolrel) exitflag = 2;
268  else if(KKTsatisf == 1) exitflag = 3;
269 
270  if( verb > 0 && (t % verb == 0 || t==1)) {
271  SG_PRINT("%d: Q_P=%m_f, Q_D=%m_f, Q_P-Q_D=%m_f, (Q_P-Q_D)/|Q_P|=%m_f \n",
272  t, Q_P, Q_D, Q_P-Q_D,(Q_P-Q_D)/CMath::abs(Q_P));
273  }
274 
275  /* Store m_UB LB to History buffer */
276  if( t < History_size ) {
277  History[INDEX(0,t,2)] = Q_P;
278  History[INDEX(1,t,2)] = Q_D;
279  }
280  else {
281  tmp_ptr=SG_MALLOC(float64_t, (History_size+HISTORY_BUF)*2);
282  memset(tmp_ptr, 0, sizeof(float64_t)*(History_size+HISTORY_BUF)*2);
283 
284  for( i = 0; i < History_size; i++ ) {
285  tmp_ptr[INDEX(0,i,2)] = History[INDEX(0,i,2)];
286  tmp_ptr[INDEX(1,i,2)] = History[INDEX(1,i,2)];
287  }
288  tmp_ptr[INDEX(0,t,2)] = Q_P;
289  tmp_ptr[INDEX(1,t,2)] = Q_D;
290 
291  History_size += HISTORY_BUF;
292  SG_FREE(History);
293  History = tmp_ptr;
294  }
295  }
296 
297  (*ptr_t) = t;
298  (*ptr_History) = History;
299 
300  SG_PRINT("QP: %f QD: %f\n", Q_P, Q_D);
301 
302  return( exitflag );
303 }
304 
305 
306 /* --------------------------------------------------------------
307 
308 Usage: exitflag = qpbsvm_scas(m_UB, m_dim, m_tmax,
309  m_tolabs, m_tolrel, m_tolKKT, x, Nabla, &t, &History, verb )
310 
311 -------------------------------------------------------------- */
313  float64_t *Nabla,
314  int32_t *ptr_t,
315  float64_t **ptr_History,
316  int32_t verb)
317 {
318  float64_t *History;
319  float64_t *col_H;
320  float64_t *tmp_ptr;
321  float64_t x_old;
322  float64_t x_new;
323  float64_t delta_x;
324  float64_t max_x=CMath::INFTY;
325  float64_t xHx;
326  float64_t Q_P;
327  float64_t Q_D;
328  float64_t xf;
329  float64_t xi_sum;
330  float64_t max_update;
331  float64_t curr_update;
332  int32_t History_size;
333  int32_t t;
334  int32_t i, j;
335  int32_t max_i=-1;
336  int32_t exitflag;
337  int32_t KKTsatisf;
338 
339  /* ------------------------------------------------------------ */
340  /* Initialization */
341  /* ------------------------------------------------------------ */
342 
343  t = 0;
344 
345  History_size = (m_tmax < HISTORY_BUF ) ? m_tmax+1 : HISTORY_BUF;
346  History=SG_MALLOC(float64_t, History_size*2);
347  memset(History, 0, sizeof(float64_t)*History_size*2);
348 
349  /* compute Q_P and Q_D */
350  xHx = 0;
351  xf = 0;
352  xi_sum = 0;
353  for(i = 0; i < m_dim; i++ ) {
354  xHx += x[i]*(Nabla[i] - m_f[i]);
355  xf += x[i]*m_f[i];
356  xi_sum += CMath::max(0.0,-Nabla[i]);
357  }
358 
359  Q_P = 0.5*xHx + xf;
360  Q_D = -0.5*xHx - m_UB*xi_sum;
361  History[INDEX(0,t,2)] = Q_P;
362  History[INDEX(1,t,2)] = Q_D;
363 
364  if( verb > 0 ) {
365  SG_PRINT("%d: Q_P=%m_f, Q_D=%m_f, Q_P-Q_D=%m_f, (Q_P-Q_D)/|Q_P|=%m_f \n",
366  t, Q_P, Q_D, Q_P-Q_D,(Q_P-Q_D)/CMath::abs(Q_P));
367  }
368 
369  exitflag = -1;
370  while( exitflag == -1 )
371  {
372  t++;
373 
374  max_update = -CMath::INFTY;
375  for(i = 0; i < m_dim; i++ ) {
376  if( m_diag_H[i] > 0 ) {
377  /* variable update */
378  x_old = x[i];
379  x_new = CMath::min(m_UB,CMath::max(0.0, x[i] - Nabla[i]/m_diag_H[i]));
380 
381  curr_update = -0.5*m_diag_H[i]*(x_new*x_new-x_old*x_old) -
382  (Nabla[i] - m_diag_H[i]*x_old)*(x_new - x_old);
383 
384  if( curr_update > max_update ) {
385  max_i = i;
386  max_update = curr_update;
387  max_x = x_new;
388  }
389  }
390  }
391 
392  x_old = x[max_i];
393  x[max_i] = max_x;
394 
395  /* update Nabla */
396  delta_x = max_x - x_old;
397  if( delta_x != 0 ) {
398  col_H = (float64_t*)get_col(max_i);
399  for(j = 0; j < m_dim; j++ ) {
400  Nabla[j] += col_H[j]*delta_x;
401  }
402  }
403 
404  /* compute Q_P and Q_D */
405  xHx = 0;
406  xf = 0;
407  xi_sum = 0;
408  KKTsatisf = 1;
409  for(i = 0; i < m_dim; i++ ) {
410  xHx += x[i]*(Nabla[i] - m_f[i]);
411  xf += x[i]*m_f[i];
412  xi_sum += CMath::max(0.0,-Nabla[i]);
413 
414  if((x[i] > 0 && x[i] < m_UB && CMath::abs(Nabla[i]) > m_tolKKT) ||
415  (x[i] == 0 && Nabla[i] < -m_tolKKT) ||
416  (x[i] == m_UB && Nabla[i] > m_tolKKT)) KKTsatisf = 0;
417  }
418 
419  Q_P = 0.5*xHx + xf;
420  Q_D = -0.5*xHx - m_UB*xi_sum;
421 
422  /* stopping conditions */
423  if(t >= m_tmax) exitflag = 0;
424  else if(Q_P-Q_D <= m_tolabs) exitflag = 1;
425  else if(Q_P-Q_D <= CMath::abs(Q_P)*m_tolrel) exitflag = 2;
426  else if(KKTsatisf == 1) exitflag = 3;
427 
428  if( verb > 0 && (t % verb == 0 || t==1)) {
429  SG_PRINT("%d: Q_P=%m_f, Q_D=%m_f, Q_P-Q_D=%m_f, (Q_P-Q_D)/|Q_P|=%m_f \n",
430  t, Q_P, Q_D, Q_P-Q_D,(Q_P-Q_D)/CMath::abs(Q_P));
431  }
432 
433  /* Store m_UB LB to History buffer */
434  if( t < History_size ) {
435  History[INDEX(0,t,2)] = Q_P;
436  History[INDEX(1,t,2)] = Q_D;
437  }
438  else {
439  tmp_ptr=SG_MALLOC(float64_t, (History_size+HISTORY_BUF)*2);
440  memset(tmp_ptr, 0, (History_size+HISTORY_BUF)*2*sizeof(float64_t));
441  for( i = 0; i < History_size; i++ ) {
442  tmp_ptr[INDEX(0,i,2)] = History[INDEX(0,i,2)];
443  tmp_ptr[INDEX(1,i,2)] = History[INDEX(1,i,2)];
444  }
445  tmp_ptr[INDEX(0,t,2)] = Q_P;
446  tmp_ptr[INDEX(1,t,2)] = Q_D;
447 
448  History_size += HISTORY_BUF;
449  SG_FREE(History);
450  History = tmp_ptr;
451  }
452  }
453 
454  (*ptr_t) = t;
455  (*ptr_History) = History;
456 
457  return( exitflag );
458 }
459 
460 /* --------------------------------------------------------------
461 
462 Usage: exitflag = qpbsvm_scamv(m_UB, m_dim, m_tmax,
463  m_tolabs, m_tolrel, m_tolKKT, x, Nabla, &t, &History, verb )
464 
465 -------------------------------------------------------------- */
467  float64_t *Nabla,
468  int32_t *ptr_t,
469  float64_t **ptr_History,
470  int32_t verb)
471 {
472  float64_t *History;
473  float64_t *col_H;
474  float64_t delta_x;
475  float64_t x_new;
476  float64_t max_viol;
477  float64_t fval;
478  int32_t t;
479  int32_t i;
480  int32_t u=-1;
481  int32_t exitflag;
482 
483  /* ------------------------------------------------------------ */
484  /* Initialization */
485  /* ------------------------------------------------------------ */
486 
487  t = 0;
488  exitflag = -1;
489  while( exitflag == -1 && t <= m_tmax)
490  {
491  t++;
492 
493  max_viol = 0;
494  for(i = 0; i < m_dim; i++ )
495  {
496  if( x[i] == 0 )
497  {
498  if( max_viol < -Nabla[i]) { u = i; max_viol = -Nabla[i]; }
499  }
500  else if( x[i] > 0 && x[i] < m_UB )
501  {
502  if( max_viol < CMath::abs(Nabla[i]) ) { u = i; max_viol = CMath::abs(Nabla[i]); }
503  }
504  else if( max_viol < Nabla[i]) { u = i; max_viol = Nabla[i]; }
505  }
506 
507 /* SG_PRINT("%d: max_viol=%m_f, u=%d\n", t, max_viol, u);*/
508 
509  if( max_viol <= m_tolKKT )
510  {
511  exitflag = 1;
512  }
513  else
514  {
515  /* update */
516  x_new = CMath::min(m_UB,CMath::max(0.0, x[u] - Nabla[u]/m_diag_H[u]));
517 
518  delta_x = x_new - x[u];
519  x[u] = x_new;
520 
521  col_H = (float64_t*)get_col(u);
522  for(i = 0; i < m_dim; i++ ) {
523  Nabla[i] += col_H[i]*delta_x;
524  }
525  }
526  }
527 
528  History=SG_MALLOC(float64_t, (t+1)*2);
529  memset(History, 0, sizeof(float64_t)*(t+1)*2);
530 
531  fval = 0;
532  for(fval = 0, i = 0; i < m_dim; i++ ) {
533  fval += 0.5*x[i]*(Nabla[i]+m_f[i]);
534  }
535 
536  History[INDEX(0,t,2)] = fval;
537  History[INDEX(1,t,2)] = 0;
538 
539  (*ptr_t) = t;
540  (*ptr_History) = History;
541 
542 
543 
544  return( exitflag );
545 }
546 
547 /* --------------------------------------------------------------
548 
549 Usage: exitflag = qpbsvm_prloqo(m_UB, m_dim, m_tmax,
550  m_tolabs, m_tolrel, m_tolKKT, x, Nabla, &t, &History, verb )
551 
552 -------------------------------------------------------------- */
554  float64_t *Nabla,
555  int32_t *ptr_t,
556  float64_t **ptr_History,
557  int32_t verb)
558 {
561  float64_t* primal=SG_MALLOC(float64_t, 3*m_dim);
562  float64_t* dual=SG_MALLOC(float64_t, 1+2*m_dim);
564 
565  for (int32_t i=0; i<m_dim; i++)
566  {
567  a[i]=0.0;
568  lb[i]=0;
569  ub[i]=m_UB;
570  }
571 
572  float64_t b=0;
573 
575  int32_t result=pr_loqo(m_dim, 1, m_f, m_H, a, &b, lb, ub, primal, dual,
576  2, 5, 1, -0.95, 10,0);
577 
578  SG_FREE(a);
579  SG_FREE(lb);
580  SG_FREE(ub);
581  SG_FREE(primal);
582  SG_FREE(dual);
583 
584  *ptr_t=0;
585  *ptr_History=NULL;
586  return result;
587 }
588 
590  float64_t *Nabla,
591  int32_t *ptr_t,
592  float64_t **ptr_History,
593  int32_t verb)
594 {
595  for (int32_t i=0; i<m_dim; i++)
596  x[i]=CMath::random(0.0, 1.0);
597 
598  for (int32_t t=0; t<200; t++)
599  {
600  for (int32_t i=0; i<m_dim; i++)
601  {
602  x[i]= (-m_f[i]-(SGVector<float64_t>::dot(x,&m_H[m_dim*i], m_dim) -
603  m_H[m_dim*i+i]*x[i]))/m_H[m_dim*i+i];
604  x[i]=CMath::clamp(x[i], 0.0, 1.0);
605  }
606  }
607 
608  int32_t atbound=0;
609  for (int32_t i=0; i<m_dim; i++)
610  {
611  if (x[i]==0.0 || x[i]==1.0)
612  atbound++;
613  }
614  SG_PRINT("atbound:%d of %d (%2.2f%%)\n", atbound, m_dim, ((float64_t) 100.0*atbound)/m_dim);
615  *ptr_t=0;
616  *ptr_History=NULL;
617  return 0;
618 }
619 
621  float64_t *Nabla,
622  int32_t *ptr_t,
623  float64_t **ptr_History,
624  int32_t verb)
625 {
626  for (int32_t i=0; i<m_dim; i++)
627  x[i]=CMath::random(0.0, 1.0);
628 
629  for (int32_t t=0; t<2000; t++)
630  {
631  for (int32_t i=0; i<m_dim; i++)
632  {
633  x[i]-=0.001*(SGVector<float64_t>::dot(x,&m_H[m_dim*i], m_dim)+m_f[i]);
634  x[i]=CMath::clamp(x[i], 0.0, 1.0);
635  }
636  }
637 
638  int32_t atbound=0;
639  for (int32_t i=0; i<m_dim; i++)
640  {
641  if (x[i]==0.0 || x[i]==1.0)
642  atbound++;
643  }
644  SG_PRINT("atbound:%d of %d (%2.2f%%)\n", atbound, m_dim, ((float64_t) 100.0*atbound)/m_dim);
645  *ptr_t=0;
646  *ptr_History=NULL;
647  return 0;
648 }
649 
650 #ifdef USE_CPLEX
651 /* --------------------------------------------------------------
652 
653 Usage: exitflag = qpbsvm_prloqo(m_UB, m_dim, m_tmax,
654  m_tolabs, m_tolrel, m_tolKKT, x, Nabla, &t, &History, verb )
655 
656 -------------------------------------------------------------- */
658  float64_t *Nabla,
659  int32_t *ptr_t,
660  float64_t **ptr_History,
661  int32_t verb)
662 {
665 
666  for (int32_t i=0; i<m_dim; i++)
667  {
668  lb[i]=0;
669  ub[i]=m_UB;
670  }
671 
672  CCplex cplex;
673  cplex.init(E_QP);
674  cplex.setup_lp(m_f, NULL, 0, m_dim, NULL, lb, ub);
675  cplex.setup_qp(m_H, m_dim);
676  cplex.optimize(x);
677  cplex.cleanup();
678 
679  SG_FREE(lb);
680  SG_FREE(ub);
681 
682  *ptr_t=0;
683  *ptr_History=NULL;
684  return 0;
685 }
686 #endif

SHOGUN Machine Learning Toolbox - Documentation