/**
* @file gensvm_kernel.c
* @author G.J.J. van den Burg
* @date 2013-10-18
* @brief Defines main functions for use of kernels in GenSVM.
*
* @details
* Functions for constructing different kernels using user-supplied
* parameters. Also contains the functions for decomposing the
* kernel matrix using several decomposition methods.
*
* @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_kernel.h"
#include "gensvm_print.h"
/**
* @brief Copy the kernelparameters from GenModel to GenData
*
* @details
* This is a little utility function to copy the kernel type and kernel
* parameters from a GenModel struct to a GenData struct.
*
* @param[in] model a GenModel struct
* @param[in] data a GenData struct
*
*/
void gensvm_kernel_copy_kernelparam_to_data(struct GenModel *model,
struct GenData *data)
{
data->kerneltype = model->kerneltype;
data->gamma = model->gamma;
data->coef = model->coef;
data->degree = model->degree;
}
/**
* @brief Do the preprocessing steps needed to perform kernel GenSVM
*
* @details
* To achieve nonlinearity through kernels in GenSVM, a preprocessing step is
* needed. This preprocessing step computes the full kernel matrix, and an
* eigendecomposition of this matrix. Next, it computes a matrix @f$\textbf{M}
* = \textbf{P}\boldsymbol{\Sigma}@f$ which takes the role as data matrix in
* the optimization algorithm.
*
* @sa
* gensvm_kernel_compute(), gensvm_kernel_eigendecomp(),
* gensvm_kernel_trainfactor(), gensvm_kernel_postprocess()
*
* @param[in] model input GenSVM model
* @param[in,out] data input structure with the data. On exit,
* contains the training factor in GenData::Z,
* and the original data in GenData::RAW
*
*/
void gensvm_kernel_preprocess(struct GenModel *model, struct GenData *data)
{
if (model->kerneltype == K_LINEAR) {
data->r = data->m;
return;
}
long r, n = data->n;
double *P = NULL,
*Sigma = NULL,
*K = NULL;
// build the kernel matrix
K = Calloc(double, n*n);
gensvm_kernel_compute(model, data, K);
// generate the eigen decomposition
r = gensvm_kernel_eigendecomp(K, n, model->kernel_eigen_cutoff, &P,
&Sigma);
// build M and set to data (leave RAW intact)
gensvm_kernel_trainfactor(data, P, Sigma, r);
// Set Sigma to data->Sigma (need it again for prediction)
if (data->Sigma != NULL) {
free(data->Sigma);
data->Sigma = NULL;
}
data->Sigma = Sigma;
// write kernel params to data
gensvm_kernel_copy_kernelparam_to_data(model, data);
free(K);
free(P);
}
/**
* @brief Compute the kernel postprocessing factor
*
* @details
* This function computes the postprocessing factor needed to do predictions
* with kernels in GenSVM. This is a wrapper around gensvm_kernel_cross() and
* gensvm_kernel_testfactor().
*
* @param[in] model a GenSVM model
* @param[in] traindata the training dataset
* @param[in,out] testdata the test dataset. On exit, GenData::Z
* contains the testfactor
*/
void gensvm_kernel_postprocess(struct GenModel *model,
struct GenData *traindata, struct GenData *testdata)
{
if (model->kerneltype == K_LINEAR) {
testdata->r = testdata->m;
return;
}
// build the cross kernel matrix between train and test
double *K2 = gensvm_kernel_cross(model, traindata, testdata);
// generate the data matrix N = K2 * M * Sigma^{-2}
gensvm_kernel_testfactor(testdata, traindata, K2);
free(K2);
}
/**
* @brief Compute the kernel matrix
*
* @details
* This function computes the kernel matrix of a data matrix based on the
* requested kernel type and the kernel parameters. The potential types of
* kernel functions are document in KernelType. This function uses a naive
* multiplication and computes the entire upper triangle of the kernel matrix,
* then copies this over to the lower triangle.
*
* @param[in] model a GenModel structure with the model
* @param[in] data a GenData structure with the data
* @param[out] K an nxn preallocated kernel matrix
*
*/
void gensvm_kernel_compute(struct GenModel *model, struct GenData *data,
double *K)
{
long i, j;
long n = data->n;
double value;
double *x1 = NULL,
*x2 = NULL;
for (i=0; iRAW[i*(data->m+1)+1];
x2 = &data->RAW[j*(data->m+1)+1];
if (model->kerneltype == K_POLY)
value = gensvm_kernel_dot_poly(x1, x2, data->m,
model->gamma, model->coef,
model->degree);
else if (model->kerneltype == K_RBF)
value = gensvm_kernel_dot_rbf(x1, x2, data->m,
model-> gamma);
else if (model->kerneltype == K_SIGMOID)
value = gensvm_kernel_dot_sigmoid(x1, x2,
data->m, model->gamma,
model->coef);
else {
// LCOV_EXCL_START
err("[GenSVM Error]: Unknown kernel type in "
"gensvm_make_kernel\n");
exit(EXIT_FAILURE);
// LCOV_EXCL_STOP
}
matrix_set(K, n, i, j, value);
matrix_set(K, n, j, i, value);
}
}
}
/**
* @brief Find the (reduced) eigendecomposition of a kernel matrix
*
* @details
* Compute the reduced eigendecomposition of the kernel matrix. This uses the
* LAPACK function dsyevx to do the computation. Only those
* eigenvalues/eigenvectors are kept for which the ratio between the
* eigenvalue and the largest eigenvalue is larger than cutoff. This function
* uses the highest precision eigenvalues, twice the underflow threshold (see
* dsyevx documentation).
*
* @param[in] K the kernel matrix
* @param[in] n the dimension of the kernel matrix
* @param[in] cutoff mimimum ratio of eigenvalue to largest
* eigenvalue for the eigenvector to be
* included
* @param[out] P_ret on exit contains the eigenvectors
* @param[out] Sigma_ret on exit contains the eigenvalues
*
* @return the number of eigenvalues kept
*/
long gensvm_kernel_eigendecomp(double *K, long n, double cutoff, double **P_ret,
double **Sigma_ret)
{
int M, status, LWORK, *IWORK = NULL,
*IFAIL = NULL;
long i, j, num_eigen, cutoff_idx;
double max_eigen, abstol, *WORK = NULL,
*Sigma = NULL,
*P = NULL;
double *tempSigma = Malloc(double, n);
double *tempP = Malloc(double, n*n);
IWORK = Malloc(int, 5*n);
IFAIL = Malloc(int, n);
// highest precision eigenvalues, may reduce for speed
abstol = 2.0*dlamch('S');
// first perform a workspace query to determine optimal size of the
// WORK array.
WORK = Malloc(double, 1);
status = dsyevx('V', 'A', 'U', n, K, n, 0, 0, 0, 0, abstol, &M,
tempSigma, tempP, n, WORK, -1, IWORK, IFAIL);
LWORK = WORK[0];
// allocate the requested memory for the eigendecomposition
WORK = (double *)realloc(WORK, LWORK*sizeof(double));
status = dsyevx('V', 'A', 'U', n, K, n, 0, 0, 0, 0, abstol, &M,
tempSigma, tempP, n, WORK, LWORK, IWORK, IFAIL);
if (status != 0) {
// LCOV_EXCL_START
err("[GenSVM Error]: Nonzero exit status from dsyevx.\n");
exit(EXIT_FAILURE);
// LCOV_EXCL_STOP
}
// Select the desired number of eigenvalues, depending on their size.
// dsyevx sorts eigenvalues in ascending order.
max_eigen = tempSigma[n-1];
cutoff_idx = 0;
for (i=0; i cutoff) {
cutoff_idx = i;
break;
}
}
num_eigen = n - cutoff_idx;
Sigma = Calloc(double, num_eigen);
for (i=0; in-1-num_eigen; j--) {
for (i=0; in;
long n_test = data_test->n;
long m = data_test->m;
double value, *x1 = NULL,
*x2 = NULL,
*K2 = Calloc(double, n_test * n_train);
for (i=0; iRAW[i*(m+1)+1];
x2 = &data_train->RAW[j*(m+1)+1];
if (model->kerneltype == K_POLY)
value = gensvm_kernel_dot_poly(x1, x2, m,
model->gamma, model->coef,
model->degree);
else if (model->kerneltype == K_RBF)
value = gensvm_kernel_dot_rbf(x1, x2, m,
model->gamma);
else if (model->kerneltype == K_SIGMOID)
value = gensvm_kernel_dot_sigmoid(x1, x2, m,
model->gamma, model->coef);
else {
// LCOV_EXCL_START
err("[GenSVM Error]: Unknown kernel type in "
"gensvm_make_crosskernel\n");
exit(EXIT_FAILURE);
// LCOV_EXCL_STOP
}
matrix_set(K2, n_train, i, j, value);
}
}
return K2;
}
/**
* @brief Compute the training factor as part of kernel preprocessing
*
* @details
* This function computes the matrix product @f$\textbf{M} =
* \textbf{P}\boldsymbol{\Sigma}@f$ and stores the result in GenData::Z,
* preceded by a column of ones. It also sets GenData::r to the number of
* eigenvectors that were includedin P and Sigma. Note that P and Sigma
* correspond to the reduced eigendecomposition of the kernel matrix.
*
* @param[in,out] data a GenData structure. On exit, GenData::Z and
* GenData::r are updated as described above.
* @param[in] P the eigenvectors
* @param[in] Sigma the eigenvalues
* @param[in] r the number of eigenvalues and eigenvectors
*/
void gensvm_kernel_trainfactor(struct GenData *data, double *P, double *Sigma,
long r)
{
long i, j, n = data->n;
double value;
// allocate Z
data->Z = Calloc(double, n*(r+1));
// Write data->Z = [1 M] = [1 P*Sigma]
for (i=0; iZ, r+1, i, j+1, value);
}
matrix_set(data->Z, r+1, i, 0, 1.0);
}
// Set data->r to r so data knows the width of Z
data->r = r;
}
/**
* @brief Calculate the matrix product for the testfactor
*
* @details
* To predict class labels when kernels are used, a transformation of the
* testdata has to be performed to get the simplex space vectors. This
* transformation is based on the matrix @f$\textbf{K}_2@f$ (as calculated by
* gensvm_make_crosskernel()) and the matrices @f$\textbf{M} =
* \textbf{P}*\boldsymbol{\Sigma}@f$) and @f$\boldsymbol{\Sigma}@f$. The
* testfactor is equal to @f$\textbf{K}_2 \textbf{M}
* \boldsymbol{\Sigma}^{-2}@f$.
*
* @param[out] testdata a GenData struct with the testdata, contains
* the testfactor in GenData::Z on exit preceded
* by a column of ones.
* @param[in] traindata a GenData struct with the training data
* @param[in] K2 crosskernel between the train and test data
*/
void gensvm_kernel_testfactor(struct GenData *testdata,
struct GenData *traindata, double *K2)
{
long n1, n2, r, i, j;
double value, *N = NULL,
*M = NULL;
n1 = traindata->n;
n2 = testdata->n;
r = traindata->r;
N = Calloc(double, n2*r);
M = Calloc(double, n1*r);
// copy M from traindata->Z because we need it in dgemm without column
// of 1's.
for (i=0; iZ, r+1, i, j+1);
matrix_set(M, r, i, j, value);
}
}
// Multiply K2 with M and store in N
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, n2, r, n1, 1.0,
K2, n1, M, r, 0.0, N, r);
// Multiply N with Sigma^{-2}
for (j=0; jSigma, 1, j, 0), -2.0);
for (i=0; iZ = Calloc(double, n2*(r+1));
for (i=0; iZ, r+1, i, j+1,
matrix_get(N, r, i, j));
}
matrix_set(testdata->Z, r+1, i, 0, 1.0);
}
// Set r to testdata
testdata->r = r;
free(M);
free(N);
}
/**
* @brief Compute the RBF kernel between two vectors
*
* @details
* The RBF kernel is computed between two vectors. This kernel is defined as
* @f[
* k(x_1, x_2) = \exp( -\gamma \| x_1 - x_2 \|^2 )
* @f]
* where @f$ \gamma @f$ is a kernel parameter specified.
*
* @param[in] x1 first vector
* @param[in] x2 second vector
* @param[in] n length of the vectors x1 and x2
* @param[in] gamma gamma parameter of the kernel
* @returns kernel evaluation
*/
double gensvm_kernel_dot_rbf(double *x1, double *x2, long n, double gamma)
{
long i;
double value = 0.0;
for (i=0; i