邻近算法,或者说K最近邻(kNN,k-NearestNeighbor)分类算法可以说是整个数据挖掘分类技术中最简单的方法了。所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用她最接近的k个邻居来代表。
kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。KNN方法在类别决策时,只与极少量的相邻样本有关。
K最近邻分类算法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。KNN方法虽然从原理上也依赖于极限定理,但在类别决策时,只与极少量的相邻样本有关。由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
KNN算法不仅可以用于分类,还可以用于回归。通过找出一个样本的k个最近邻居,将这些邻居的属性的平均值赋给该样本,就可以得到该样本的属性。更有用的方法是将不同距离的邻居对该样本产生的影响给予不同的权值(weight),如权值与距离成反比。
KNN算法的缺点:
1. 当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。可以采用权值的方法(和该样本距离小的邻居权值大)来改进。
2. 计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本。该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分。
KNN算法的改进策略:
在以上的处理过程中,所有的临近K值对结果点的影响效果是一样的,不管这个点离它有多远。而在实际应用中,我们可以采取附加权值的方法,放大临近点对结果的影响。
KNN算法的步骤:
a.计算已知类别数据集中每个点与当前点的距离;
b.选取与当前点距离最小的K个点;
c.统计前K个点中每个类别的样本出现的频率;
d.返回前K个点出现频率最高的类别作为当前点的预测分类。
OPENCV的实现
OPENCV中使用CvKNearest类实现KNN分类。虽然该类中有train成员函数,但是该函数和boost以及tree中的train不一样。前者仅仅是将train中训练数据,重新保存在CvKNearest类中的成员变量samples中。后者则是根据一系列的训练规则生成一个或者很多个决策树。因此CvKNearest类可以不使用train函数,在CvKNearest类中重载了构造函数,在构造函数中传入训练数据,能达到和train相同的作用。
CvKNearest类的find_nearest成员函数,计算了最相近的K个样本,从而分析出待测样本的类别。find_nearest成员变量类似于boost以及tree中的predict的功能。
virtual float find_nearest( const CvMat* samples, int k, CV_OUT CvMat* results=0,
const float** neighbors=0, CV_OUT CvMat* neighborResponses=0, CV_OUT CvMat* dist=0 ) const;
1. samples为样本数*特征数的浮点矩阵;
2. K为寻找最近点的个数;
3. results为样本数个预测结果;
4. neibhbors为k*样本数个指针数组,即每个样本的K个近邻的指针(输入为const,实在不知为何如此设计);
5. neighborResponse为样本数*k个预测结果,即每个样本K个近邻的输出值;
6. dist为样本数*k个距离,即每个样本K个近邻的距离。
find_nearest函数的核心是调用下面的符号重载函数。
void operator()( const cv::Range& range ) const{
cv::AutoBuffer<float> buf(buf_sz);
for(int i = range.start; i < range.end; i += 1 )
{
float* neighbor_responses = &buf[0];
float* dist = neighbor_responses + 1*k;
Cv32suf* sort_buf = (Cv32suf*)(dist + 1*k);
pointer->find_neighbors_direct( _samples, k, i, i + 1,
neighbor_responses, _neighbors, dist );
float r = pointer->write_results( k, k1, i, i + 1, neighbor_responses, dist,
_results, _neighbor_responses, _dist, sort_buf );
if( i == 0 )
*result = r;
}
}
其中find_neighbors_direct实现了计算样本和训练数据的距离,并找出最近的K个训练样本。write_results实现了在最近的K个样本中找出数目最多的类别(或者回归的值)。
下面介绍一下find_neighbors_direct。
void CvKNearest::find_neighbors_direct( const CvMat* _samples, int k, int start, int end,float* neighbor_responses, const float** neighbors, float* dist ) const
{
int i, j, count = end - start, k1 = 0, k2 = 0, d = var_count; //d为特征个数
CvVectors* s = samples;// samples为训练集上的样本
for( ; s != 0; s = s->next )
{
int n = s->count;
for( j = 0; j < n; j++ ) //计算输入样本和所有训练集上的样本距离
{
for( i = 0; i < count; i++ )
{
double sum = 0;
Cv32suf si;
const float* v = s->data.fl[j]; // v为训练集上的某个样本
const float* u = (float*)(_samples->data.ptr + _samples->step*(start + i)); // u为输入的某个样本
Cv32suf* dd = (Cv32suf*)(dist + i*k);
float* nr;
const float** nn;
int t, ii, ii1;
//计算输入的某个样本和训练集上某个样本的距离sum,
//即在特征空间上计算两个样本向量的欧式距离
for( t = 0; t <= d - 4; t += 4 )
{
double t0 = u[t] - v[t], t1 = u[t+1] - v[t+1];
double t2 = u[t+2] - v[t+2], t3 = u[t+3] - v[t+3];
sum += t0*t0 + t1*t1 + t2*t2 + t3*t3;
}
for( ; t < d; t++ )
{
double t0 = u[t] - v[t];
sum += t0*t0;
}
si.f = (float)sum; //输入的某个样本和训练集上某个样本的距离
//dd为输入的某个样本和训练集上样本的距离,这里只保存最小的K个
//dd是从小到大排序的,因此从后向前(从K-1~到0)比较sum,
//找到比sum小的距离,即找到sum的插入位置
//K1为当前dd中保存的有效元素个数,K1-1为最后面元素的下标
for( ii = k1-1; ii >= 0; ii-- )
if( si.i > dd[ii].i )
break;
if( ii >= k-1 ) //dd已经存储了K个,sum比dd[K-1]大,sum无需存储
continue;
nr = neighbor_responses + i*k; //nr存储K个训练集样本的标签
nn = neighbors ? neighbors + (start + i)*k : 0;
//如果找到sum的插入位置是dd的中间位置,需要将dd后面的所有元素向后依次顺移一位,空出插入位置
//如dd有10个元素,dd[0]最小,dd[9]最大.当sum比dd[3]大时,则sum要插入dd[4], 这就同时需要dd[4]
//移到dd[5], dd[5]移到dd[6]…
//K2为当前dd中保存的有效元素个数,但当dd存放了K个元素时,K2=K-1
// K2-1为dd最后面元素的下标,当dd存满时,K2-1对应倒数第二个元素
//即当dd存满时,原来最后一个元素丢弃,用原来倒数第二个元素代替
//当dd未存满时,K2-1对应倒数第一个元素,此时,先将倒数第一个元素(ii1)移位到下面的空位中(ii1+1)
//sum为比dd[ii]大,即sum应该插入到dd[ii+1], ii+1为插入的位置,即只移动插入位置之后的元素
for( ii1 = k2 - 1; ii1 > ii; ii1-- )
{
dd[ii1+1].i = dd[ii1].i;
nr[ii1+1] = nr[ii1];
if( nn ) nn[ii1+1] = nn[ii1];
}//实现在插入位置处,后面元素的移位,对应标签的移位
//在插入位置处插入对应值,sum的插入位置为ii+1,sum为比dd[ii]大,即sum应该插入到dd[ii+1]
dd[ii+1].i = si.i;
nr[ii+1] = ((float*)(s + 1))[j];
if( nn )
nn[ii+1] = v;
}
k1 = MIN( k1+1, k );//K1为当前dd中保存的有效元素个数,
k2 = MIN( k1, k-1 ); //K2为当前dd中保存的有效元素个数,不超过K-1
}
}
}