源于数据挖掘的一个做业, 这里用Node.js来实现一下这个机器学习中最简单的算法之一k-nearest-neighbor算法(k最近邻分类法)。node
仍是先严谨的介绍下。急切学习法(eager learner)是在接受待分类的新元组以前就构造了分类模型,学习后的模型已经就绪,急着对未知的元组进行分类,因此称为急切学习法,诸如决策树概括,贝叶斯分类等都是急切学习法的例子。惰性学习法(lazy learner)正好与其相反,直到给定一个待接受分类的新元组以后,才开始根据训练元组构建分类模型,在此以前只是存储着训练元组,因此称为惰性学习法,惰性学习法在分类进行时作更多的工做。git
本文的knn算法就是一种惰性学习法,它被普遍应用于模式识别。knn基于类比学习,将未知的新元组与训练元组进行对比,搜索模式空间,找出最接近未知元组的k个训练元组,这里的k便是knn中的k。这k个训练元祖就是待预测元组的k个最近邻。github
balabala了这么多,是否是某些同窗想大喊一声..speak Chinese! 仍是来通俗的解释下,而后再来看上面的理论应该会明白不少。小时候妈妈会指着各类各样的东西教咱们,这是小鸭子,这个红的是苹果等等,那咱们哼哧哼哧的看着应答着,屡次被教后再看到的时候咱们本身就能认出来这些事物了。主要是由于咱们在脑海像给这个苹果贴了不少标签同样,不仅是颜色这一个标签,可能还有苹果的形状大小等等。这些标签让咱们看到苹果的时候不会误认为是橘子。其实这些标签就对应于机器学习中的特征这一重要概念,而训练咱们识别的过程就对应于泛化这一律念。一台iphone戴了一个壳或者屏幕上有一道划痕,咱们仍是能认得出来它,这对于咱们人来讲很是简单,但蠢计算机就不知道怎么作了,须要咱们好好调教它,固然也不能过分调教2333,过分调教它要把其余手机也认成iphone那就很差了,其实这就叫过分泛化。算法
因此特征就是提取对象的信息,泛化就是学习到隐含在这些特征背后的规律,并对新的输入给出合理的判断。npm
咱们能够看上图,绿色的圆表明未知样本,咱们选取距离其最近的k个几何图形,这k个几何图形就是未知类型样本的邻居,若是k=3,咱们能够看到有两个红色的三角形,有一个蓝色的三正方形,因为红色三角形所占比例高,因此咱们能够判断未知样本类型为红色三角形。扩展到通常状况时,这里的距离就是咱们根据样本的特征所计算出来的数值,再找出距离未知类型样本最近的K个样本,便可预测样本类型。那么求距离其实不一样状况适合不一样的方法,咱们这里采用欧式距离。数组
综上所述knn分类的关键点就是k的选取和距离的计算。数据结构
个人数据是一个xls文件,那么我去npm搜了一下选了一个叫node-xlrd的包直接拿来用。dom
// node.js用来读取xls文件的包 var xls = require('node-xlrd');
而后直接看文档copy实例便可,把数据解析后插入到本身的数据结构里。iphone
var data = []; // 将文件中的数据映射到样本的属性 var map = ['a','b','c','d','e','f','g','h','i','j','k']; // 读取文件 xls.open('data.xls', function(err,bk){ if(err) {console.log(err.name, err.message); return;} var shtCount = bk.sheet.count; for(var sIdx = 0; sIdx < shtCount; sIdx++ ){ var sht = bk.sheets[sIdx], rCount = sht.row.count, cCount = sht.column.count; for(var rIdx = 0; rIdx < rCount; rIdx++){ var item = {}; for(var cIdx = 0; cIdx < cCount; cIdx++){ item[map[cIdx]] = sht.cell(rIdx,cIdx); } data.push(item); } } // 等文件读取完毕后 执行测试 run(); });
而后定义一个构造函数Sample表示一个样本,这里是把刚生成的数据结构里的对象传入,生成一个新的样本。机器学习
// Sample表示一个样本 var Sample = function (object) { // 把传过来的对象上的属性克隆到新建立的样本上 for (var key in object) { // 检验属性是否属于对象自身 if (object.hasOwnProperty(key)) { this[key] = object[key]; } } }
再定义一个样本集的构造函数
// SampleSet管理全部样本 参数k表示KNN中的k var SampleSet = function(k) { this.samples = []; this.k = k; }; // 将样本加入样本数组 SampleSet.prototype.add = function(sample) { this.samples.push(sample); }
而后咱们会在样本的原型上定义不少方法,这样每一个样本均可以用这些方法。
// 计算样本间距离 采用欧式距离 Sample.prototype.measureDistances = function(a, b, c, d, e, f, g, h, i, j, k) { for (var i in this.neighbors) { var neighbor = this.neighbors[i]; var a = neighbor.a - this.a; var b = neighbor.b - this.b; var c = neighbor.c - this.c; var d = neighbor.d - this.d; var e = neighbor.e - this.e; var f = neighbor.f - this.f; var g = neighbor.g - this.g; var h = neighbor.h - this.h; var i = neighbor.i - this.i; var j = neighbor.j - this.j; var k = neighbor.k - this.k; // 计算欧式距离 neighbor.distance = Math.sqrt(a*a + b*b + c*c + d*d + e*e + f*f + g*g + h*h + i*i + j*j + k*k); } }; // 将邻居样本根据与预测样本间距离排序 Sample.prototype.sortByDistance = function() { this.neighbors.sort(function (a, b) { return a.distance - b.distance; }); }; // 判断被预测样本类别 Sample.prototype.guessType = function(k) { // 有两种类别 1和-1 var types = { '1': 0, '-1': 0 }; // 根据k值截取邻居里面前k个 for (var i in this.neighbors.slice(0, k)) { var neighbor = this.neighbors[i]; types[neighbor.trueType] += 1; } // 判断邻居里哪一个样本类型多 if(types['1']>types['-1']){ this.type = '1'; } else { this.type = '-1'; } }
注意到我这里的数据有a-k共11个属性,样本有1和-1两种类型,使用truetype和type来预测样本类型和对比判断是否分类成功。
最后是样本集的原型上定义一个方法,该方法能够在整个样本集里寻找未知类型的样本,并生成他们的邻居集,调用未知样本原型上的方法来计算邻居到它的距离,把全部邻居按距离排序,最后猜想类型。
// 构建总样本数组,包含未知类型样本 SampleSet.prototype.determineUnknown = function() { /* * 一旦发现某个未知类型样本,就把全部已知的样本 * 克隆出来做为该未知样本的邻居序列。 * 之因此这样作是由于咱们须要计算该未知样本和全部已知样本的距离。 */ for (var i in this.samples) { // 若是发现没有类型的样本 if ( ! this.samples[i].type) { // 初始化未知样本的邻居 this.samples[i].neighbors = []; // 生成邻居集 for (var j in this.samples) { // 若是碰到未知样本 跳过 if ( ! this.samples[j].type) continue; this.samples[i].neighbors.push( new Sample(this.samples[j]) ); } // 计算全部邻居与预测样本的距离 this.samples[i].measureDistances(this.a, this.b, this.c, this.d, this.e, this.f, this.g, this.h, this.k); // 把全部邻居按距离排序 this.samples[i].sortByDistance(); // 猜想预测样本类型 this.samples[i].guessType(this.k); } } };
最后分别计算10倍交叉验证和留一法交叉验证的精度。
留一法就是每次只留下一个样本作测试集,其它样本作训练集。
K倍交叉验证将全部样本分红K份,通常均分。取一份做为测试样本,剩余K-1份做为训练样本。这个过程重复K次,最后的平均测试结果能够衡量模型的性能。
k倍验证时定义了个方法先把数组打乱随机摆放。
// helper函数 将数组里的元素随机摆放 function ruffle(array) { array.sort(function (a, b) { return Math.random() - 0.5; }) }
剩余测试代码好写,这里就不贴了。
测试结果为
计算邻居到未知样本的距离主要有欧氏距离、余弦距离、汉明距离、曼哈顿距离等方式,this case用余弦距离等计算方式可能精度会更高。
knn算法很是简单,但却能在不少关键的地方发挥做用而且效果很是好。缺点就是进行分类时要扫描全部训练样本获得距离,训练集大的话会很慢。
看似高大上的机器学习其实就是基于计算机科学,统计学,数学的一些实现,相信经过这个最简单的入门算法能让咱们对机器学习有一点小小的感知和体会。
完整代码和文件在: https://github.com/Lunaticf/D...