/**
* @file gensvm_optimize.c
* @author G.J.J. van den Burg
* @date 2013-08-09
* @brief Main functions for training the GenSVM solution.
*
* @details
* Contains update and loss functions used to actually find
* the optimal V.
*
* @copyright
Copyright 2016, G.J.J. van den Burg.
This file is part of GenSVM.
GenSVM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
GenSVM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GenSVM. If not, see .
*/
#include "gensvm_optimize.h"
/**
* Iteration frequency with which to print to stdout
*/
#ifndef GENSVM_PRINT_ITER
#define GENSVM_PRINT_ITER 100
#endif
/**
* @brief The main training loop for GenSVM
*
* @details
* This function is the main training function. This function
* handles the optimization of the model with the given model parameters, with
* the data given. On return the matrix GenModel::V contains the optimal
* weight matrix.
*
* In this function, step doubling is used in the majorization algorithm after
* a burn-in of 50 iterations.
*
* @param[in,out] model the GenModel to be trained. Contains optimal
* V on exit.
* @param[in] data the GenData to train the model with.
*/
void gensvm_optimize(struct GenModel *model, struct GenData *data)
{
long it = 0;
double L, Lbar, acc;
long n = model->n;
long m = model->m;
long K = model->K;
// initialize the workspace
struct GenWork *work = gensvm_init_work(model);
// print some info on the dataset and model configuration
note("Starting main loop.\n");
note("Dataset:\n");
note("\tn = %i\n", n);
note("\tm = %i\n", m);
note("\tK = %i\n", K);
note("Parameters:\n");
note("\tkappa = %f\n", model->kappa);
note("\tp = %f\n", model->p);
note("\tlambda = %15.16f\n", model->lambda);
note("\tepsilon = %g\n", model->epsilon);
note("\n");
// compute necessary simplex vectors
gensvm_simplex(model);
gensvm_simplex_diff(model);
// get initial loss
L = gensvm_get_loss(model, data, work);
Lbar = L + 2.0*model->epsilon*L;
// run main loop
while ((it < model->max_iter) && (Lbar - L)/L > model->epsilon)
{
// ensures V contains newest V and Vbar contains V from
// previous
gensvm_get_update(model, data, work);
if (it > 50)
gensvm_step_doubling(model);
Lbar = L;
L = gensvm_get_loss(model, data, work);
if (it % GENSVM_PRINT_ITER == 0) {
gensvm_predict_labels(data, model, work->yhat);
acc = gensvm_prediction_perf(data, work->yhat);
note("iter = %li, L = %15.16f, Lbar = %15.16f, "
"reldiff = %15.16f, acc = %.2f\n", it, L, Lbar,
(Lbar - L)/L, acc);
}
it++;
}
// status == 0 means training was successful
model->status = 0;
// print warnings if necessary
if (L > Lbar) {
err("[GenSVM Warning]: Negative step occurred in "
"majorization.\n");
model->status = 1;
}
if (it >= model->max_iter) {
err("[GenSVM Warning]: maximum number of iterations "
"reached.\n");
model->status = 2;
}
// compute final training accuracy
gensvm_predict_labels(data, model, work->yhat);
acc = gensvm_prediction_perf(data, work->yhat);
// print final iteration count and loss
note("Optimization finished, iter = %li, loss = %15.16f, "
"rel. diff. = %15.16f, acc = %.2f\n", it-1, L,
(Lbar - L)/L, acc);
// compute and print the number of SVs in the model
note("Number of support vectors: %li\n", gensvm_num_sv(model));
// store the training error in the model
model->training_error = (Lbar - L)/L;
// store the iteration count in the model
model->elapsed_iter = it - 1;
// free the workspace
gensvm_free_work(work);
}
/**
* @brief Calculate the current value of the loss function
*
* @details
* The current loss function value is calculated based on the matrix V in the
* given model. Note that the matrix ZV is passed explicitly to avoid having
* to reallocate memory at every step.
*
* @param[in] model GenModel structure which holds the current
* estimate V
* @param[in] data GenData structure
* @param[in] work allocated workspace with the ZV matrix to use
* @returns the current value of the loss function
*/
double gensvm_get_loss(struct GenModel *model, struct GenData *data,
struct GenWork *work)
{
long i, j;
long n = model->n;
long K = model->K;
long m = model->m;
double value, rowvalue, loss = 0.0;
gensvm_calculate_errors(model, data, work->ZV);
gensvm_calculate_huber(model);
for (i=0; iy[i]-1))
continue;
value = matrix_get(model->H, K, i, j);
value = pow(value, model->p);
rowvalue += value;
}
rowvalue = pow(rowvalue, 1.0/(model->p));
rowvalue *= model->rho[i];
loss += rowvalue;
}
loss /= ((double) n);
value = 0;
for (i=1; iV, K-1, i, j), 2.0);
}
}
loss += model->lambda * value;
return loss;
}
/**
* @brief Use step doubling
*
* @details
* Step doubling can be used to speed up the maorization algorithm. Instead of
* using the value at the minimimum of the majorization function, the value
* ``opposite'' the majorization point is used. This can essentially cut the
* number of iterations necessary to reach the minimum in half.
*
* @param[in] model GenModel containing the augmented parameters
*/
void gensvm_step_doubling(struct GenModel *model)
{
long i, j;
double value;
long m = model->m;
long K = model->K;
for (i=0; iV, K-1, i, j, 2.0);
value = - matrix_get(model->Vbar, K-1, i, j);
matrix_add(model->V, K-1, i, j, value);
}
}
}
/**
* @brief Calculate the Huber hinge errors
*
* @details
* For each of the scalar errors in Q the Huber hinge errors are
* calculated. The Huber hinge is here defined as
* @f[
* h(q) =
* \begin{dcases}
* 1 - q - \frac{\kappa + 1}{2} & \text{if } q \leq
* -\kappa \\
* \frac{1}{2(\kappa + 1)} ( 1 - q)^2 & \text{if } q \in
* (-\kappa, 1] \\
* 0 & \text{if } q > 1
* \end{dcases}
* @f]
*
* @param[in,out] model the corresponding GenModel
*/
void gensvm_calculate_huber(struct GenModel *model)
{
long i, j;
double q, value;
for (i=0; in; i++) {
for (j=0; jK; j++) {
q = matrix_get(model->Q, model->K, i, j);
value = 0.0;
if (q <= -model->kappa) {
value = 1.0 - q - (model->kappa+1.0)/2.0;
} else if (q <= 1.0) {
value = 1.0/(2.0*model->kappa+2.0)*pow(1.0 - q,
2.0);
}
matrix_set(model->H, model->K, i, j, value);
}
}
}
/**
* @brief Calculate the scalar errors
*
* @details
* Calculate the scalar errors q based on the current estimate of V, and
* store these in Q. It is assumed that the memory for Q has already been
* allocated. In addition, the matrix ZV is calculated here. It is assigned
* to a pre-allocated block of memory, which is passed to this function.
*
* @param[in,out] model the corresponding GenModel
* @param[in] data the corresponding GenData
* @param[in,out] ZV a pointer to a memory block for ZV. On exit
* this block is updated with the new ZV matrix
* calculated with GenModel::V
*/
void gensvm_calculate_errors(struct GenModel *model, struct GenData *data,
double *ZV)
{
long i, j;
double q, *uu_row = NULL;
long n = model->n;
long K = model->K;
gensvm_calculate_ZV(model, data, ZV);
for (i=0; iy[i]-1))
continue;
uu_row = &model->UU[((data->y[i]-1)*K+j)*(K-1)];
q = cblas_ddot(K-1, &ZV[i*(K-1)], 1, uu_row, 1);
matrix_set(model->Q, K, i, j, q);
}
}
}