k近鄰算法能夠說概念上很簡單,即:「給定一個訓練數據集,對新的輸入實例,在訓練數據集中找到與這個實例最鄰近的k個實例,這k個實例的多數屬於某個類,就把該輸入分爲這個類。」其中我認爲距離度量最關鍵,可是距離度量的方法也很簡單,最長用的就是歐氏距離,其餘的距離度量準則實際上就是不一樣的向量範數,這部分我就不贅述了,畢竟這系列博客的重點是實現。代碼地址:https://github.com/bBobxx/statistical-learningnode
k近鄰算法的思想很簡單,然而,再簡單的概念若是碰上高維度加上海量數據,就變得很麻煩,若是按照常規思想,將每一個測試樣本和訓練樣本的距離算出來,在進行排序查找,無疑效率十分低下,這也就是爲何要介紹kd樹的緣由。kd樹是一種二叉樹,kd樹的每一個結點對應一個k維超矩形區域。 kd樹的k是k維空間,k近鄰算法的k是k個最近值,不是同樣的!看文字很抽象,其實很好理解,看圖c++
每一次分割都須要肯定一個軸和一個值,而後分割時只看該軸的數據,小於等於分割值就放到該結點的左子樹裏,大於分割值就放到右子樹中。那麼每一個結點裏面須要存儲哪些內容呢?git
個人實現裏面,每一個結點有以下內容:github
struct KdtreeNode { vector<double> val;//n維特徵 int cls;//類別 unsigned long axis;//分割軸 double splitVal;//分割的值 vector<vector<double>> leftTreeVal;//左子樹的值集合 vector<vector<double>> rightTreeVal;//右子樹的值集合 KdtreeNode* parent;//父節點 KdtreeNode* left;//左子節點 KdtreeNode* right;//右子節點 KdtreeNode(): cls(0), axis(0), splitVal(0.0), parent(nullptr), left(nullptr), right(nullptr){}; };
用kd樹實現的k近鄰算法(還有其它的方法),訓練過程實際上就是樹的建造過程,咱們用遞歸建立kd樹。算法
首先,咱們須要建立並存儲根節點函數
KdtreeNode* root = new KdtreeNode();//類中用這個存儲根節點 void Knn::setRoot() {//這是建立根節點的程序,主要是設定左右子樹,還有分割軸,分割值 if(axisVec.empty()){ cout<<"please run createSplitAxis first."<<endl; throw axisVec.empty(); } auto axisv = axisVec; auto axis = axisv.top(); axisv.pop(); std::sort(trainData.begin(), trainData.end(), [&axis](vector<double> &left, vector<double > &right) { return left[axis]<right[axis]; }); unsigned long mid = trainData.size()/2; for(unsigned long i = 0; i < trainData.size(); ++i){ if(i!=mid){ if (i<mid) root->leftTreeVal.push_back(trainData[i]); else root->rightTreeVal.push_back(trainData[i]); } else{ root->val.assign(trainData[i].begin(),trainData[i].end()-1); root->splitVal = trainData[i][axis]; root->axis = axis; root->cls = *(trainData[i].end()-1); } } cout<<"root node set over"<<endl; }
上面的程序建立了根節點,可是分割軸是怎麼肯定?固然能夠依次選軸做爲分割軸,可是這裏咱們選擇按方差從大到小的順序選軸性能
stack<unsigned long> axisVec;//用棧存儲分割軸,棧頂軸方差最大。 void Knn::createSplitAxis(){//axisVec建立代碼 cout<<"createSplitAxis..."<<endl; //the last element of trainData is gt vector<pair<unsigned long, double>> varianceVec; auto sumv = trainData[0]; for(unsigned long i=1;i<trainData.size();++i){ sumv = sumv + trainData[i]; } auto meanv = sumv/trainData.size(); vector<decltype(trainData[0]-meanv)> subMean; for(const auto& c:trainData) subMean.push_back(c-meanv); for (unsigned long i = 0; i < trainData.size(); ++i) { for (unsigned long j = 0; j < indim; ++j) { subMean[i][j] *= subMean[i][j]; } } auto varc = subMean[0]; for(unsigned long i=1;i<subMean.size();++i){ varc = varc + subMean[i]; } auto var = varc/subMean.size(); for(unsigned long i=0;i<var.size()-1;++i){//here not contain the axis of gt varianceVec.push_back(pair<unsigned long, double>(i, var[i])); } std::sort(varianceVec.begin(), varianceVec.end(), [](pair<unsigned long, double> &left, pair<unsigned long, double> &right) { return left.second < right.second; }); for(const auto& variance:varianceVec){ axisVec.push(variance.first);//the maximum variance is on the top } cout<<"createSplitAxis over"<<endl; }
如今要給根節點添加左右子樹:學習
root->left = buildTree(root, root->leftTreeVal, axisVec); root->right = buildTree(root, root->rightTreeVal, axisVec);
來看一下buildTree代碼:測試
KdtreeNode* Knn::buildTree(KdtreeNode*root, vector<vector<double>>& data, stack<unsigned long>& axisStack) {//第一個參數是父節點,第二個參數是目前沒有被分割的數據集合,第三個參數是當前的軸棧, //因爲後面要保證左右子樹的分割用的同一個軸,因此這裏要傳入。 stack<unsigned long> aS; if(axisStack.empty()) aS=axisVec; else aS=axisStack; auto node = new KdtreeNode(); node->parent = root; auto axis2 = aS.top(); aS.pop(); std::sort(data.begin(), data.end(), [&axis2](vector<double> &left, vector<double > &right) { return left[axis2]<right[axis2]; });//這裏用的c++11裏面的lambda函數 unsigned long mid = data.size()/2; if(node->leftTreeVal.empty()&&node->rightTreeVal.empty()){ for(unsigned long i = 0; i < data.size(); ++i){ if(i!=mid){ if (i<mid) node->leftTreeVal.push_back(data[i]); else node->rightTreeVal.push_back(data[i]); } else{ node->val.assign(data[i].begin(),data[i].end()-1); node->splitVal = data[i][axis2]; node->axis = axis2; node->cls = *(data[i].end()-1); } } } if(!node->leftTreeVal.empty()){ node->left = buildTree(node, node->leftTreeVal, aS);//遞歸創建子樹 } if(!node->rightTreeVal.empty()){ node->right = buildTree(node, node->rightTreeVal, aS); } return node; }
創建好子樹後能夠經過showTree函數前序遍歷樹來查看,這裏就不演示了,代碼中有這一步。優化
對於用kd樹實現的Knn算法來講,預測的過程就是查找的過程,這裏咱們給出查找K個最近鄰的代碼,中間用到了STL標準模板庫的priority_queue和pair的組合,用priority_queue實現大頂堆,對於由pair構成的priority_queue來講,默認的比較值是first,也就是說裏面的元素會根據pair的第一個元素從大到小排序,即用.top()獲得的是最大值(默認比較函數的狀況下)。在搜索 K-近鄰時,設置一個有 k
個元素的大頂堆,創建樹時,當堆不滿時,將結點和距離放入,堆滿時,只需比較當前搜索點的 dis
是否小於堆頂點的 dis
,若是小於,堆頂出堆,並將當前搜索點壓入。
priority_queue<pair<double, KdtreeNode*>> maxHeap;
下面給出查找代碼
void Knn::findKNearest(vector<double>& testD){ ...//前面略過,避免代碼過長。。。 if(testDF[curNparent->axis]<=curNparent->splitVal)//從這裏開始是爲了查找同一個父節點的 //另外一個子樹中是否有比當前K個最近鄰更近的結點 curNchild = curNparent->right;//這裏和上面相反,恰好是另外一個子樹。 else curNchild = curNparent->left; if(curNchild == nullptr) continue; double childDis = computeDis(testDF, curNchild->val); if(childDis<maxHeap.top().first){//比較另外一個子樹的根節點是否是比當前k個結點距離查找點更近, //若是是,將對應的子樹加入搜索路徑 maxHeap.pop(); maxHeap.push(pair<double, KdtreeNode*>(childDis, curNchild)); while(curNchild!= nullptr){//add subtree to path path.push(curNchild); if(testD[curNchild->axis]<=curNchild->splitVal) curNchild = curNchild->left; else curNchild = curNchild->right; } } } } double Knn::computeDis(const vector<double>& v1, const vector<double>& v2){ auto v = v1 - v2; double di = v*v;//這裏用到了基類中的操做符重載 return di; }
k近鄰算法雖然概念簡單,可是實現因爲要用到樹結構,編寫起來仍是挺具備挑戰性的,之後還會進行性能的優化,慢慢來。