CvSVM::predict函数解析:不管是Mat接口还是CvMat接口最终都是通过指针的形式调用的,也就是最终都是调用的以下函数实现的
float CvSVM::predict( const float* row_sample, int row_len, bool returnDFVal ) const
{
// 首先确保创建了核函数输入了样本
assert( kernel );
assert( row_sample );
// 样本的长度,也就是特征的维数必须匹配
int var_count = get_var_count();
assert( row_len == var_count );
(void)row_len; // 不知道干啥的
// 计算类别数目
int class_count = class_labels ? class_labels->cols :
params.svm_type == ONE_CLASS ? 1 : 0;
float result = 0;
cv::AutoBuffer<float> _buffer(sv_total + (class_count+1)*2);
float* buffer = _buffer;
// 对于回归或者一类使用以下函数
if( params.svm_type == EPS_SVR ||
params.svm_type == NU_SVR ||
params.svm_type == ONE_CLASS )
{
CvSVMDecisionFunc* df = (CvSVMDecisionFunc*)decision_func;
int i, sv_count = df->sv_count;
double sum = -df->rho;
// 计算最优平面与当前样本点之间的距离
kernel->calc( sv_count, var_count, (const float**)sv, row_sample, buffer );
for( i = 0; i < sv_count; i++ )
sum += buffer[i]*df->alpha[i];
result = params.svm_type == ONE_CLASS ? (float)(sum > 0) : (float)sum;
}
// 对于分类问题使用以下方法
else if( params.svm_type == C_SVC ||
params.svm_type == NU_SVC )
{
CvSVMDecisionFunc* df = (CvSVMDecisionFunc*)decision_func;
int* vote = (int*)(buffer + sv_total);
int i, j, k;
memset( vote, 0, class_count*sizeof(vote[0]));
// 计算样本点到所有支持向量之间的距离,sum(xi-yi),其中xi表示样本点,yi表示支持向量,i=1:N,N表示一个样本的特征维度
kernel->calc( sv_total, var_count, (const float**)sv, row_sample, buffer );
double sum = 0.;
// 对于二分类问题以下函数只执行一遍
for( i = 0; i < class_count; i++ )
{
for( j = i+1; j < class_count; j++, df++ )
{
// 获取样本点到最优分界面的距离:buffer中存放测试样本与每个支持向量距离度量值
sum = -df->rho;
int sv_count = df->sv_count;
for( k = 0; k < sv_count; k++ )
sum += df->alpha[k]*buffer[df->sv_index[k]];
// sv_index存储的是支持向量index,对于2分类实际sv_index[k]=k
// sum大于0表示属于第一个类别,也就是类别标记较小的那个类别,对于二分类问题也就是负样本
vote[sum > 0 ? i : j]++;
}
}
// 统计每个类别的投票次数,
注意大于0投票给了0也就是0为负样本,如果一次也没有投票的话,默认输出的也是负样本
for( i = 1, k = 0; i < class_count; i++ )
{
if( vote[i] > vote[k] )
k = i;
}
// returnDFVal只对于2分类问题有效,sum大于0表示属于类别标记较小的那个类别也就是-1,sum小于0表示属于类别标记较大的那个类别也就是+1
result = returnDFVal && class_count == 2 ? (float)sum : (float)(class_labels->data.i[k]);
}
else
CV_Error( CV_StsBadArg, "INTERNAL ERROR: Unknown SVM type, "
"the SVM structure is probably corrupted" );
return result;
}
kernel->calc( sv_total, var_count, (const float**)sv, row_sample, buffer );
计算样本点到支持向量点间的距离,结果存储到buffer中,以线性核函数为例
从上面的分析可以看出,如果不更改rho或者alpha的符号,计算的是-rho+w*x的结果,大于0表示负样本,小于0表示正样本;因此在HOGDescriptor中需要将alpha更改符号,直接保存rho即可,这样给出的结果rho-alpha*x,大于0表示正样本!
w=alpha*sv; alpha中存放每个支持向量的权重,sv表示每个支持向量,w为加权后的支持向量
void CvSVMKernel::calc( int vcount, int var_count, const float** vecs,
const float* another, Qfloat* results )
{
const Qfloat max_val = (Qfloat)(FLT_MAX*1e-3);
int j;
// 对于核函数为Liner时调用线性核函数进行计算
(this->*calc_func)( vcount, var_count, vecs, another, results );
// 检查是否越界
for( j = 0; j < vcount; j++ )
{
if( results[j] > max_val )
results[j] = max_val;
}
}
线性核函数又调用non_rbf_base核函数进行计算
void CvSVMKernel::calc_linear( int vcount, int var_count, const float** vecs,
const float* another, Qfloat* results )
{
calc_non_rbf_base( vcount, var_count, vecs, another, results, 1, 0 );
}
vcount表示支持向量的个数,也就是位于正负样本分界面上的正负样本总数
var_count表示样本的特征维度
vecs表示支持向量,也就是位于正负样本分解面上的正负样本
another表示测试样本
result表示对于每个支持向量的累加结果值
void CvSVMKernel::calc_non_rbf_base( int vcount, int var_count, const float** vecs,
const float* another, Qfloat* results,
double alpha, double beta )
{
int j, k;
// 对于每一个支持向量,或者每一个位于分界面的训练样本
for( j = 0; j < vcount; j++ )
{
// 获取第j个支持向量或者说第j个样本
const float* sample = vecs[j];
double s = 0;
// 特征维度大于4的时候进行优化
for( k = 0; k <= var_count - 4; k += 4 )
s += sample[k]*another[k] + sample[k+1]*another[k+1] +
sample[k+2]*another[k+2] + sample[k+3]*another[k+3];
// 计算其余的维度
for( ; k < var_count; k++ )
s += sample[k]*another[k];
// 计算测试样本与第j个样本之间的距离
results[j] = (Qfloat)(s*alpha + beta); // 注意这里的alpha和beta为每个支持向量的权重,对于线性核函数而言alpha=1,beta=0可以从
calc_linear函数看出
}
}
// RBF核函数,遵循RBF核函数公式
void CvSVMKernel::calc_rbf( int vcount, int var_count, const float** vecs,
const float* another, Qfloat* results )
{
CvMat R = cvMat( 1, vcount, QFLOAT_TYPE, results );
double gamma = -params->gamma;
int j, k;
// vcount=sv_count
for( j = 0; j < vcount; j++ )
{
const float* sample = vecs[j];
double s = 0;
// var_count=feature_num
for( k = 0; k <= var_count - 4; k += 4 )
{
double t0 = sample[k] - another[k];
double t1 = sample[k+1] - another[k+1];
s += t0*t0 + t1*t1;
t0 = sample[k+2] - another[k+2];
t1 = sample[k+3] - another[k+3];
s += t0*t0 + t1*t1;
}
for( ; k < var_count; k++ )
{
double t0 = sample[k] - another[k];
s += t0*t0;
}
results[j] = (Qfloat)(s*gamma);
}
if( vcount > 0 )
cvExp( &R, &R );
}
// 根据交叉次数自动训练函数,CvParamGrid为训练参数构造函数,其包含三个参数(min_val,max_val,step)
// Param Grid的检查函数,最大值不小于最小值,最小值不小于DBL_EPSILON,step不小于1(应该是不小于等于1)
bool CvParamGrid::check() const
{
bool ok = false;
CV_FUNCNAME( "CvParamGrid::check" );
__BEGIN__;
if( min_val > max_val )
CV_ERROR( CV_StsBadArg, "Lower bound of the grid must be less then the upper one" );
if( min_val < DBL_EPSILON )
CV_ERROR( CV_StsBadArg, "Lower bound of the grid must be positive" );
if( step < 1. + FLT_EPSILON )
CV_ERROR( CV_StsBadArg, "Grid step must greater then 1" );
ok = true;
__END__;
return ok;
}
如果不确定参数的值的范围可以通过以下函数获得,当然也可以通过一次次实验不断调整,最后得到一个大概的范围后采用train_auto函数进行训练
CvParamGrid CvSVM::get_default_grid( int param_id )
{
CvParamGrid grid;
if( param_id == CvSVM::C )
{
grid.min_val = 0.1;
grid.max_val = 500;
grid.step = 5; // total iterations = 5
}
else if( param_id == CvSVM::GAMMA )
{
grid.min_val = 1e-5;
grid.max_val = 0.6;
grid.step = 15; // total iterations = 4
}
else if( param_id == CvSVM::P )
{
grid.min_val = 0.01;
grid.max_val = 100;
grid.step = 7; // total iterations = 4
}
else if( param_id == CvSVM::NU )
{
grid.min_val = 0.01;
grid.max_val = 0.2;
grid.step = 3; // total iterations = 3
}
else if( param_id == CvSVM::COEF )
{
grid.min_val = 0.1;
grid.max_val = 300;
grid.step = 14; // total iterations = 3
}
else if( param_id == CvSVM::DEGREE )
{
grid.min_val = 0.01;
grid.max_val = 4;
grid.step = 7; // total iterations = 3
}
else
cvError( CV_StsBadArg, "CvSVM::get_default_grid", "Invalid type of parameter "
"(use one of CvSVM::C, CvSVM::GAMMA et al.)", __FILE__, __LINE__ );
return grid;
}
bool CvSVM::train_auto( const CvMat* _train_data, const CvMat* _responses,
const CvMat* _var_idx, const CvMat* _sample_idx, CvSVMParams _params, int k_fold,
CvParamGrid C_grid, CvParamGrid gamma_grid, CvParamGrid p_grid,
CvParamGrid nu_grid, CvParamGrid coef_grid, CvParamGrid degree_grid,
bool balanced)
{
bool ok = false;
CvMat* responses = 0;
CvMat* responses_local = 0;
CvMemStorage* temp_storage = 0;
const float** samples = 0;
const float** samples_local = 0;
CV_FUNCNAME( "CvSVM::train_auto" );
__BEGIN__;
int svm_type, sample_count, var_count, sample_size;
int block_size = 1 << 16;
double* alpha;
RNG* rng = &theRNG();
// all steps are logarithmic and must be > 1
// 步长都必须大于1,因为等于1的时候会造成死循环!,默认step=10
double degree_step = 10, g_step = 10, coef_step = 10, C_step = 10, nu_step = 10, p_step = 10;
double gamma = 0, curr_c = 0, degree = 0, coef = 0, p = 0, nu = 0;
double best_degree = 0, best_gamma = 0, best_coef = 0, best_C = 0, best_nu = 0, best_p = 0;
float min_error = FLT_MAX, error;
// SVMType为ONE_CLASS的时候不能进行自动训练
if( _params.svm_type == CvSVM::ONE_CLASS )
{
if(!train( _train_data, _responses, _var_idx, _sample_idx, _params ))
EXIT;
return true;
}
clear();
// 选择的交叉验证层数必须大于等于2,等于2的时候就是使用一个训练另外一个测试,训练两次,测试两次
// 等于10表示训练10次,测试10次,选择一组测试其余9组训练
if( k_fold < 2 )
CV_ERROR( CV_StsBadArg, "Parameter <k_fold> must be > 1" );
CV_CALL(set_params( _params ));
svm_type = _params.svm_type;
// All the parameters except, possibly, <coef0> are positive.
// <coef0> is nonnegative
// 检查各个参数的值是否满足要求,这是第一步检查(步长不能小于1),后面根据SVM类型与Kernel类型还会进行二次检查
if( C_grid.step <= 1 )
{
C_grid.min_val = C_grid.max_val = params.C;
C_grid.step = 10;
}
else
CV_CALL(C_grid.check());
if( gamma_grid.step <= 1 )
{
gamma_grid.min_val = gamma_grid.max_val = params.gamma;
gamma_grid.step = 10;
}
else
CV_CALL(gamma_grid.check());
if( p_grid.step <= 1 )
{
p_grid.min_val = p_grid.max_val = params.p;
p_grid.step = 10;
}
else
CV_CALL(p_grid.check());
if( nu_grid.step <= 1 )
{
nu_grid.min_val = nu_grid.max_val = params.nu;
nu_grid.step = 10;
}
else
CV_CALL(nu_grid.check());
if( coef_grid.step <= 1 )
{
coef_grid.min_val = coef_grid.max_val = params.coef0;
coef_grid.step = 10;
}
else
CV_CALL(coef_grid.check());
if( degree_grid.step <= 1 )
{
degree_grid.min_val = degree_grid.max_val = params.degree;
degree_grid.step = 10;
}
else
CV_CALL(degree_grid.check());
// these parameters are not used:
// 二次检查参数的值,根据核函数类型与SVM类型优化参数
if( params.kernel_type != CvSVM::POLY )
degree_grid.min_val = degree_grid.max_val = params.degree;
if( params.kernel_type == CvSVM::LINEAR )
gamma_grid.min_val = gamma_grid.max_val = params.gamma;
if( params.kernel_type != CvSVM::POLY && params.kernel_type != CvSVM::SIGMOID )
coef_grid.min_val = coef_grid.max_val = params.coef0;
if( svm_type == CvSVM::NU_SVC || svm_type == CvSVM::ONE_CLASS )
C_grid.min_val = C_grid.max_val = params.C;
if( svm_type == CvSVM::C_SVC || svm_type == CvSVM::EPS_SVR )
nu_grid.min_val = nu_grid.max_val = params.nu;
if( svm_type != CvSVM::EPS_SVR )
p_grid.min_val = p_grid.max_val = params.p;
CV_ASSERT( g_step > 1 && degree_step > 1 && coef_step > 1);
CV_ASSERT( p_step > 1 && C_step > 1 && nu_step > 1 );
/* Prepare training data and related parameters */
// 实现数据的转存,放到指针中
CV_CALL(cvPrepareTrainData( "CvSVM::train_auto", _train_data, CV_ROW_SAMPLE,
svm_type != CvSVM::ONE_CLASS ? _responses : 0,
svm_type == CvSVM::C_SVC ||
svm_type == CvSVM::NU_SVC ? CV_VAR_CATEGORICAL :
CV_VAR_ORDERED, _var_idx, _sample_idx,
false, &samples, &sample_count, &var_count, &var_all,
&responses, &class_labels, &var_idx ));
sample_size = var_count*sizeof(samples[0][0]);
// make the storage block size large enough to fit all
// the temporary vectors and output support vectors.
block_size = MAX( block_size, sample_count*(int)sizeof(CvSVMKernelRow));
block_size = MAX( block_size, sample_count*2*(int)sizeof(double) + 1024 );
block_size = MAX( block_size, sample_size*2 + 1024 );
CV_CALL( storage = cvCreateMemStorage(block_size + sizeof(CvMemBlock) + sizeof(CvSeqBlock)));
CV_CALL(temp_storage = cvCreateChildMemStorage(storage));
CV_CALL(alpha = (double*)cvMemStorageAlloc(temp_storage, sample_count*sizeof(double)));
create_kernel();
create_solver();
{
const int testset_size = sample_count/k_fold;
// 每组测试集合大小
const int trainset_size = sample_count - testset_size;
// 每组训练集合大小
const int last_testset_size = sample_count - testset_size*(k_fold-1);
// 最后一组测试集合大小,实际为testset_size
const int last_trainset_size = sample_count - last_testset_size;
// 最后一组训练集合大小,实际trainset_size
const bool is_regression = (svm_type == EPS_SVR) || (svm_type == NU_SVR);
size_t resp_elem_size = CV_ELEM_SIZE(responses->type);
size_t size = 2*last_trainset_size*sizeof(samples[0]);
samples_local = (const float**) cvAlloc( size );
memset( samples_local, 0, size );
responses_local = cvCreateMat( 1, trainset_size, CV_MAT_TYPE(responses->type) );
cvZero( responses_local );
// randomly permute samples and responses
// 随机变更样本和标签的顺序为了获取分组
for(int i = 0; i < sample_count; i++ )
{
int i1 = (*rng)(sample_count);
int i2 = (*rng)(sample_count);
const float* temp;
float t;
int y;
CV_SWAP( samples[i1], samples[i2], temp );
if( is_regression )
CV_SWAP( responses->data.fl[i1], responses->data.fl[i2], t );
else
CV_SWAP( responses->data.i[i1], responses->data.i[i2], y );
}
// 如果是分类问题,并且是2分类,并且需要均衡化分组
if (!is_regression && class_labels->cols==2 && balanced)
{
// count class samples
// responses中存放0和1,class_label中存放为0和1就对了,因此注意负样本的标签为0,正样本的标签为1!!
int num_0=0,num_1=0;
for (int i=0; i<sample_count; ++i)
{
if (responses->data.i[i]==class_labels->data.i[0])
++num_0;
else
++num_1;
}
// 哪个类别是较大的
int label_smallest_class;
int label_biggest_class;
if (num_0 < num_1)
{
label_biggest_class = class_labels->data.i[1];
label_smallest_class = class_labels->data.i[0];
}
else
{
label_biggest_class = class_labels->data.i[0];
label_smallest_class = class_labels->data.i[1];
int y;
CV_SWAP(num_0,num_1,y);
}
const double class_ratio = (double) num_0/sample_count;
// calculate class ratio of each fold
indexedratio *ratios=0;
ratios = (indexedratio*) cvAlloc(k_fold*sizeof(*ratios));
for (int k=0, i_begin=0; k<k_fold; ++k, i_begin+=testset_size)
{
int count0=0;
int count1=0;
int i_end = i_begin + (k<k_fold-1 ? testset_size : last_testset_size);
for (int i=i_begin; i<i_end; ++i)
{
if (responses->data.i[i]==label_smallest_class)
++count0;
else
++count1;
}
ratios[k].ind = k;
ratios[k].count_smallest = count0;
ratios[k].count_biggest = count1;
ratios[k].eval();
}
// initial distance
qsort(ratios, k_fold, sizeof(ratios[0]), icvCmpIndexedratio);
double old_dist = 0.0;
for (int k=0; k<k_fold; ++k)
old_dist += abs(ratios[k].val-class_ratio);
double new_dist = 1.0;
// iterate to make the folds more balanced
while (new_dist > 0.0)
{
if (ratios[0].count_biggest==0 || ratios[k_fold-1].count_smallest==0)
break; // we are not able to swap samples anymore
// what if we swap the samples, calculate the new distance
ratios[0].count_smallest++;
ratios[0].count_biggest--;
ratios[0].eval();
ratios[k_fold-1].count_smallest--;
ratios[k_fold-1].count_biggest++;
ratios[k_fold-1].eval();
qsort(ratios, k_fold, sizeof(ratios[0]), icvCmpIndexedratio);
new_dist = 0.0;
for (int k=0; k<k_fold; ++k)
new_dist += abs(ratios[k].val-class_ratio);
if (new_dist < old_dist)
{
// swapping really improves, so swap the samples
// index of the biggest_class sample from the minimum ratio fold
int i1 = ratios[0].ind * testset_size;
for ( ; i1<sample_count; ++i1)
{
if (responses->data.i[i1]==label_biggest_class)
break;
}
// index of the smallest_class sample from the maximum ratio fold
int i2 = ratios[k_fold-1].ind * testset_size;
for ( ; i2<sample_count; ++i2)
{
if (responses->data.i[i2]==label_smallest_class)
break;
}
// swap
const float* temp;
int y;
CV_SWAP( samples[i1], samples[i2], temp );
CV_SWAP( responses->data.i[i1], responses->data.i[i2], y );
old_dist = new_dist;
}
else
break; // does not improve, so break the loop
}
cvFree(&ratios);
}
// 遍历每个参数进行测试,将最小错误率的检测结果保存下来
// 在自动话训练时是按照如下方式进行的,当然针对不同的核函数与不同的SVM类型进行了优化
while(cur_val=min_val; cur_val<=max_val; cur_val*=step)
int* cls_lbls = class_labels ? class_labels->data.i : 0;
curr_c = C_grid.min_val;
do
{
params.C = curr_c;
gamma = gamma_grid.min_val;
do
{
params.gamma = gamma;
p = p_grid.min_val;
do
{
params.p = p;
nu = nu_grid.min_val;
do
{
params.nu = nu;
coef = coef_grid.min_val;
do
{
params.coef0 = coef;
degree = degree_grid.min_val;
do
{
params.degree = degree;
float** test_samples_ptr = (float**)samples;
uchar* true_resp = responses->data.ptr;
int test_size = testset_size;
int train_size = trainset_size;
// 根据每轮训练中最小错误率保存结果
// 一组测试,其余组用于训练,根据总的错误率作为最佳结果
error = 0;
for(int k = 0; k < k_fold; k++ )
{
// 分段拷贝训练样本与训练样本标签
memcpy( samples_local, samples, sizeof(samples[0])*test_size*k );
memcpy( samples_local + test_size*k, test_samples_ptr + test_size,
sizeof(samples[0])*(sample_count - testset_size*(k+1)) );
memcpy( responses_local->data.ptr, responses->data.ptr, resp_elem_size*test_size*k );
memcpy( responses_local->data.ptr + resp_elem_size*test_size*k,
true_resp + resp_elem_size*test_size,
resp_elem_size*(sample_count - testset_size*(k+1)) );
if( k == k_fold - 1 )
{
test_size = last_testset_size;
train_size = last_trainset_size;
responses_local->cols = last_trainset_size;
}
// Train SVM on <train_size> samples
// 分组测试
if( !do_train( svm_type, train_size, var_count,
(const float**)samples_local, responses_local, temp_storage, alpha ) )
EXIT;
// Compute test set error on <test_size> samples
// 统计错误样本个数
for(int i = 0; i < test_size; i++, true_resp += resp_elem_size, test_samples_ptr++ )
{
float resp = predict( *test_samples_ptr, var_count );
error += is_regression ? powf( resp - *(float*)true_resp, 2 )
: ((int)resp != cls_lbls[*(int*)true_resp]);
}
}
// 保存具有最小错误样本个数的训练参数
if( min_error > error )
{
min_error = error;
best_degree = degree;
best_gamma = gamma;
best_coef = coef;
best_C = curr_c;
best_nu = nu;
best_p = p;
}
degree *= degree_grid.step;
}
while( degree < degree_grid.max_val );
coef *= coef_grid.step;
}
while( coef < coef_grid.max_val );
nu *= nu_grid.step;
}
while( nu < nu_grid.max_val );
p *= p_grid.step;
}
while( p < p_grid.max_val );
gamma *= gamma_grid.step;
}
while( gamma < gamma_grid.max_val );
curr_c *= C_grid.step;
}
while( curr_c < C_grid.max_val );
}
// 计算错误率
min_error /= (float) sample_count;
params.C = best_C;
params.nu = best_nu;
params.p = best_p;
params.gamma = best_gamma;
params.degree = best_degree;
params.coef0 = best_coef;
// 根据最佳的参数做一次最后的训练,因此分组测试的目的是为了寻找最佳参数
CV_CALL(ok = do_train( svm_type, sample_count, var_count, samples, responses, temp_storage, alpha ));
__END__;
delete solver;
solver = 0;
cvReleaseMemStorage( &temp_storage );
cvReleaseMat( &responses );
cvReleaseMat( &responses_local );
cvFree( &samples );
cvFree( &samples_local );
if( cvGetErrStatus() < 0 || !ok )
clear();
return ok;
}
总结:
1) predict函数首先计算测试样本到每个支持向量sv[j]之间的距离,然后乘以每个支持向量的权重alpha[i]结果得到一个数,该值减去rho,如果大于0表示为负样本,否则为正样本
2) 对于二分类使用balanced=true的train_auto而言,负样本标签必须为0,这样就可以通过统计正负样本的比例而得到一个较好的平衡结果