先给出一个可以运行的KNN的代码。大家可以先通过运行这个代码,熟悉一下这个简单的分类方法的内容。
#include <iostream>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <fstream>
#include <sstream>
#include <cassert>
#include <cmath>
using namespace std;
//样例结构体,所属类型和特征向量
struct sample
{
string type;
vector<double> features;
};
// 类型和距离结构体,未用到
struct typeDistance
{
string type;
double distance;
};
bool operator < (const typeDistance& lhs, const typeDistance& rhs)
{
return lhs.distance < rhs.distance;
}
// 读取训练样本
// 训练样本的格式是:每行代表一个样例
// 每行的第一个元素是类型名,后面的是样例的特征向量
// 例如:
/*
a 1 2 3 4 5
b 5 4 3 2 1
c 3 3 3 3 3
d -3 -3 -3 -3 -3
a 1 2 3 4 4
b 4 4 3 2 1
c 3 3 3 2 4
d 0 0 1 1 -2
*/
/*
从file所指示的文件中读取训练的样本。标签,以及相应属性的值。
*/
void readTrain(vector<sample>& train, const string& file)
{
/*
关于c_str()
1. c_str():生成一个const char*指针,指向以空字符终止的数组。
①这个数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。因此要么现用先转换,要么把它的数据复制到用户自己可以管理的内存中。
其实上面的c = s.c_str(); 不是一个好习惯。既然c指针指向的内容容易失效,我们就应该按照上面的方法,那怎么把数据复制出来呢?这就要用到strcpy等函数(推荐)。
② c_str()返回一个客户程序可读不可改的指向字符数组的指针,不需要手动释放或删除这个指针
data():与c_str()类似,但是返回的数组不以空字符终止。
*/
ifstream fin(file.c_str());
if (!fin)
{
cerr << "File error!" << endl;
exit(1);
}
string line;
double d = 0.0;
while (getline(fin, line))
{
/*
C++中有三种字符串流,分别是istringstream ,ostringstream,stringstream,
分别处理字符串流的输入,输出,和输入输出。istringstream sin(s);
定义一个字符串输入流的对象sin,并调用sin的复制构造函数,将s中所包含的字符串放入sin 对象中!
*/
istringstream sin(line);
sample ts;
sin >> ts.type;
while (sin >> d)
{
ts.features.push_back(d);
}
//一行一个样本,把所有的样本都输入到train向量中。每个元素是一个样本。
train.push_back(ts);
}
fin.close();
}
// 读取测试样本
// 每行代表一个样例
// 每一行是一个样例的特征向量
// 例如:
/*
1 2 3 2 4
2 3 4 2 1
8 7 2 3 5
-3 -2 2 4 0
-4 -4 -4 -4 -4
1 2 3 4 4
4 4 3 2 1
3 3 3 2 4
0 0 1 1 -2
*/
void readTest(vector<sample>& test, const string& file)
{
ifstream fin(file.c_str());
if (!fin)
{
cerr << "File error!" << endl;
exit(1);
}
double d = 0.0;
string line;
while (getline(fin, line))
{
istringstream sin(line);
//处理一个样本ts
sample ts;
while (sin >> d)
{
ts.features.push_back(d);
}
//放入样本集合test
test.push_back(ts);
}
//关闭文件
fin.close();
}
// 计算欧氏距离
double euclideanDistance(const vector<double>& v1, const vector<double>& v2)
{
assert(v1.size() == v2.size());
double ret = 0.0;
/*
size_type由string类类型和vector类类型定义的类型,用以保存任意string对象或vector对象的长度,标准库类型将size_type定义为unsigned类型
*/
for (vector<double>::size_type i = 0; i != v1.size(); ++i)
{
ret += (v1[i] - v2[i]) * (v1[i] - v2[i]);
}
return sqrt(ret);
}
// 初始化距离矩阵
// 该矩阵是根据训练样本和测试样本而得
// 矩阵的行数为测试样本的数目,列数为训练样本的数目
// 每一行为一个测试样本到各个训练样本之间的欧式距离组成的数组
void initDistanceMatrix(vector<vector<double> >& dm, const vector<sample>& train, const vector<sample>& test)
{
for (vector<sample>::size_type i = 0; i != test.size(); ++i)
{
vector<double> vd;
for (vector<sample>::size_type j = 0; j != train.size(); ++j)
{
vd.push_back(euclideanDistance(test[i].features, train[j].features));
}
dm.push_back(vd);
}
for (vector<vector<double> >::size_type s = 0; s!= dm.size(); s++) {
vector<double> vd = dm[s];
for (vector<double>::size_type sz = 0; sz< vd.size(); sz++) {
printf("%.4f ", vd[sz]);
}
printf("\n");
}
}
// K-近邻法的实现
// 设定不同的 k 值,给每个测试样例予以一个类型
// 距离和权重成反比
void knnProcess(vector<sample>& test, const vector<sample>& train, const vector<vector<double> >& dm, unsigned int k)
{
//挨个取出test样本集中的每个样本
for (vector<sample>::size_type i = 0; i != test.size(); ++i)
{
/*
multimap的特点为key是可以重复的,而普通map中的key是不可以重复的。
multimap<int, CString>mapTest;
multimap<int, CString>::iterator pIter;
遍历,主要思路为根据key,multimap的特点为key是可以重复,即一个key对应多个value。将所有key取出来,然后每个key逐个遍历。
使用map/multimap之前要加入头文件#include<map>,map和multimap将key/value当作元素,进行管理。它们可根据key的排序准则自动将元素排序。
multimap允许重复元素,map不允许重复元素
*/
/*
对应于样本集test中的第i个样本所对应的与每个训练集中每一个训练样本的距离向量为dm[i]
*/
multimap<double, string> dts; //保存与测试样本i距离最近的k个点
for (vector<double>::size_type j = 0; j != dm[i].size(); ++j)
{
if (dts.size() < k) //把前面k个插入dts中
{
dts.insert(make_pair(dm[i][j], train[j].type)); //插入时会自动排序,按dts中的double排序,最小的排在最后
}
else
{
//dts.end()指向的是下一个元素的下一个指针位置。因此使用时需要--
multimap<double, string>::iterator it = dts.end();
--it;
if (dm[i][j] < it->first) //把当前测试样本i到当前训练样本之间的欧氏距离与dts中最小距离比较,若更小就更新dts
{
/*
2.大小、判断空函数
int size() const:返回容器元素个数
bool empty() const:判断容器是否空,若返回true,表明容器已空。
3.增加删除函数
iterator insert(const value_type& x):插入元素x
iterator insert(iterator it,const value_type& x):在迭代指针it处插入元素x
void insert(const value_type *first,const value_type* last):插入[first, last)之间元素
iterator erase(iterator it):删除迭代指针it处元素
iterator erase(iterator first,iterator last):删除[first, last)之间元素
size_type erase(const Key& key):删除键值等于key的元素
*/
dts.erase(it);
dts.insert(make_pair(dm[i][j], train[j].type));
}
}
}
map<string, double> tds;
string type = "";
double weight = 0.0;
//下面for循环主要是求出与测试样本i最邻近的k个样本点中大多数属于的类别,即将其作为测试样本点i的类别
for (multimap<double, string>::const_iterator cit = dts.begin(); cit != dts.end(); ++cit)
{
// 不考虑权重的情况,在 k 个样例中只要出现就加 1
// ++tds[cit->second];
// 这里是考虑距离与权重的关系,距离越大权重越小.其中cit->first表示距离,而cit->second表示训练集合的类型。也就是前面的字符。
tds[cit->second] += 1.0 / cit->first;
//weight表示该测试样本所属的某个训练样本集类别的权重。
if (tds[cit->second] > weight)
{
weight = tds[cit->second];
type = cit->second; //保存一下类别
}
}
test[i].type = type;
}
}
// 输出结果
// 输出的格式和训练样本的格式一样
// 每行表示一个样例,第一个元素是该样例的类型,后面是该样例的特征向量
// 例如:
/*
a 1 2 3 2 4
b 2 3 4 2 1
b 8 7 2 3 5
a -3 -2 2 4 0
d -4 -4 -4 -4 -4
a 1 2 3 4 4
b 4 4 3 2 1
c 3 3 3 2 4
d 0 0 1 1 -2
*/
/*
此处test应该为KNN过程之后所计算出来的test
*/
void writeTest(const vector<sample>& test, const string& file)
{
ofstream fout(file.c_str());
if (!fout)
{
cerr << "File error!" << endl;
exit(1);
}
for (vector<sample>::size_type i = 0; i != test.size(); ++i)
{
fout << test[i].type << '\t';
for (vector<double>::size_type j = 0; j != test[i].features.size(); ++j)
{
fout << test[i].features[j] << ' ';
}
fout << endl;
}
}
// 封装
void knn(const string& file1, const string& file2, const string& file3, int k)
{
vector<sample> train, test;
readTrain(train, file1.c_str());
readTest(test, file2.c_str());
vector<vector<double> > dm;
//计算每个test中的测试样本到每一个训练样本中的距离
initDistanceMatrix(dm, train, test);
knnProcess(test, train, dm, k);
writeTest(test, file3.c_str());
}
// 测试
int main()
{
knn("train.txt", "test.txt", "result.txt", 5);
printf("Do you want to see the results!");
char ch;
scanf("%c", &ch);
if (ch == 'q')
{
return 0;
}
return 0;
}
/*
邻近算法,或者说K最近邻(kNN,k-NearestNeighbor)分类算法可以说是整个数据挖掘分类技术中最简单的方法了。
所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用她最接近的k个邻居来代表。
kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,
并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。
KNN算法的决策过程
KNN算法的决策过程
K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。
该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,
则该样本也属于这个类别。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的
一个或者几个样本的类别来决定待分样本所属的类别。 KNN方法虽然从原理上也依赖于极限定理,但在类别决策时,
只与极少量的相邻样本有关。由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的
,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
KNN算法不仅可以用于分类,还可以用于回归。通过找出一个样本的k个最近邻居,将这些邻居的属性的平均值赋给该样本,
就可以得到该样本的属性。更有用的方法是将不同距离的邻居对该样本产生的影响给予不同的权值(weight),如权值与距离成反比。
*/
KNN属于基于实例的方法,只是简单的把训练样例存储起来。从这些事例中泛化的工作中被推迟到必须分类新的实例时。每当遇到一个新的查询实例,它分析这个新实例与以前存储的实例的关系,并据此把一个目标函数值赋给新实例。基于实例的方法还包括最近邻算法nearest neighbour和局部加权回归locally weighted regression法。他们都假定实例可以表示为欧式空间中的点。基于实例的方法有时又被称为消极(lazy)学习方法,因为它们把处理工作延迟到必须分类新的实例时。最近邻算法和局部加权回归法用于逼近实值和离散目标函数。基于实例的方法与其他类型的分类方法具有一个关键的差异就是:基于实例的方法可以为每个不同的待分类查询实例建立不同的目标函数逼近。
基于实例方法的不足是,分类新实例的开销可能很大。这是因为几乎所有的计算都发生在分类时,即计算距离矩阵和选择k个最近邻居的计算中,而不是在第一粗遇到训练样例时。如何有效的索引训练样例,以减少查询时所需的计算是重要的实践问题。第二个不足,尤其是KNN,当从存储器中检索相似的训练样例时,它们一般考虑实例的所有属性。如果目标概念仅仅依赖于很多属性中的几个时,那么真正的“相似”实例之间的距离可能相距甚远。
基于实例的学习方法中最基本的就是k-临近算法。这个算法假定所有的实例对应于n维空间中的点。一个实例的最近邻根据标准欧式距离定义。更精确说,任意的实例x可以表示为下面的特征向量
x <a1(x), a2(x), ..., an(x)>
其中ar(x)表示实例x的第r个属性值。两个实例xi和xj的距离d(xi, xj)为
在最近邻学习中,目标函数值可以是离散值也可以是实值。先考虑离散目标函数 其中V是有限集合{v1, v2, v3, .., vs}就是我们所谓的分类。蘑菇有毒,五毒。颜色中的红橙黄绿。
下图给出了逼近离散函数 的k-近邻算法:
-------------------------------------------------------------
训练算法:
对于每个训练样本<x, f(x)>,把这个样本加入列表training_examples分类算法:
给定一个要分类的查询实例
在 training_examples中选出最靠近的k个实例,并用x1 ... xk 表示
返回
</pre><pre class="html" name="code"> 其中 , 如果 a = b, 那么δ(a, b) = 1,否则δ(a, b) = 0
-------------------------------------------------------------
直观上理解,就是先有一个训练样本集合。然后有测试样本集合或者单个的测试样本。我们首先通过进行每个测试样本与所有训练样本的距离进行计算,得出距离矩阵。也就是代码中的(N*M)矩阵。N为测试样本集合的大小,M为训练样本集合的大小。求得距离矩阵dm的过程可以二次遍历。即首先依次从test_examples中选取一个待测样本,这为一层循环,然后从training_examples中选出每一个训练样本中的样本求出距离。这是第二层的循环。对应initDistance函数
选择最靠近 的k个最近邻样本 则对应于knnProcess函数中的第一个for循环。它把k个最近邻保存在了 multimap<double, string> dts; //保存与测试 样本i距离最近的k个点 dts中的元素即为距离的k个近邻。(属性值和标签对应)
返回的逼近离散函数则是通过knnProcess中的第二个for循环保存在了map<string, double> tds; 中。其中键值对分别为分类-距离。
注意:K-近邻算法的隐含考虑的假设空间H的特性是这样的,它从来不形成关于目标函数f的明确的一般假设它仅仅在需要时计算每个新查询实例的分类。
待续...
K-临近算法隐含的假设空间H的特性是什么?注意:K近邻算法从来没形成关于目标函数f的明确的一般假设函数f冒。它仅仅在需要时计算每个新查询实例的分类。然而我们依然可以问隐含的一般函数是什么?或者说,如果保持训练样例不变并用X中的每个可能实例查询,会得到什么样的分类。上图右画出了1近邻算法在整个实力空间上导致的决策面形状。决策面是围绕每个训练样例的凸多边形的合并。对于每个训练样例,多变形指出了一个查询点的集合,它的分类完全由相应的训练样例决定。在这个多边形外的查询点更接近其他的训练样例。这种类型的图经常被称为这个训练样例集合的Voronoi图。而左图则给出了一系列的正反训练样本和一个待分类项。1-近邻算法会把代分类项分类为正例,而5邻近算法会把待分类项<img alt="" src="https://img-blog.csdn.net/20151123093808561?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" />分类为反例。对前面k邻近算法作简单的修改后,它就可以被用于逼近连续值的目标函数。为了实现这一点,我们让算法计算k个最邻近样例的平均值,而不是计算最普遍的值。更精确定讲,为了逼近一个实值函数<img alt="" src="https://img-blog.csdn.net/20151123095239022?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" />,只要把算法中的公式替换为:
距离加权算法:
实际上上面的代码就是普通的KNN算法的改进版本,即距离加权算法。这种明显的改进是对k个近邻的贡献加权,根据它们相对于
的距离,将较大的权值复制给较近的近邻。例如在上图算法中,我们可以根据每个近邻与
的距离平凡的导数加权这个近邻的“选举权”。方法通过下式取代算法中的公式:
其中:
为了处理查询点
签好匹配某个训练样例xi,从而导致分母为0的情况,我们令这种情况下的f冒(
)等于f(xi)。如果有多个这样的训练样例,我们使用它们中占多数的分类。我们也可以用类似的公式对实值目标函数进行距离加权,用下面的公式替换逼近的实值目标函数:
其中wi的定义与上式一致。注意这个式子中的分母是一个常亮,将不同的权值贡献归一化。(例如,它如果保证对所有的训练样例xi,f(xi) = c,那么
)
注意:以上K-邻近算法的所有辩题都只考虑k个邻近用以分类查询点。如果使用按距离加权,那么允许所有的训练样例影响
的分类事实上没有坏处,因为非常远的实例对
(
)的影响很小。考虑所有样例的唯一不足是使运行变的更慢。如果分类一个新的查询实例,考虑所有的样本,我们称为全局global法。如果仅仅考虑最靠近的训练样例,我们称它为局部local法。当式子逼近的实值目标函数用于全局法时,它被称为Shepard法(Shepard 1986)