这是对大神的文章自己研究记录的笔记。链接:https://www.jianshu.com/p/29ebbb12230e
大神涉及的领域极多,而且内容十分有趣,建议像我这样的新手看看他的文章。
算法原理
KNN是通过测量不同特征值之间的距离进行分类。它的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别,其中K通常是不大于20的整数。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。(转https://www.cnblogs.com/ybjourney/p/4702562.html的解释,当初接触KNN算法是在看《机器学习实战》,怕解释不清楚引用别人的文章)
更通俗来说,计算新数据点和已经分类的数据点距离,从中选中K个最近的点,然后查看K个点中哪些点最多,最多的点对应的类别就是新数据点的类别。K值的选取原则(忘光了,待我查找资料再补充)
来人,上代码!
/*
* @desc Knn算法
* @param {Object} current
* @param {Array} points
* @param {Number} k
* @param {Function} c
* @return {Array}
*/
// 函数目的是为了得到最靠近的k个点,还有分类标识
function getKnn(current, points, labelX, labelY, k, c) {
var dists = [];//存放最接近的
var classify = [];//分类标识
points.map(function (item) {
// 存储分类标识
if (classify.indexOf(item[labelY]) == -1) classify.push(item[labelY]);
var result = {};
result.p = item;
// console.log(result.p);
result.d = c(current, item[labelX]);
dists.push(result);
});
dists.sort(function (a, b) {//排序
return a.d - b.d;
});
return { dists: dists.slice(0, k), classify: classify };
}
/*
* @desc 决策
* @param {Object} current 输入值
* @param {Object} points 训练样本集
* @param {Object} labelX 用于分类的输入值
* @param {Object} labelY 用于分类的输出值
* @param {Number} k 用于选择最近邻的数目
* @param {Function} c 自定义比较函数
* @return {Object}
*/
function classify0(current, points, labelX, labelY, k, c) {
var result = [];
var knn = getKnn(current, points, labelX, labelY, k, c);
// 最接近的k个点
var dists = knn.dists;
for (var i of knn.classify) {
// 存储特征种类,并把特征出现的次数置为0
result.push({
label: i,
value: 0
});
}
dists.map(function (item) {
for (var i of result) {
// k个最接近点特征进行统计
if (i.label === item.p[labelY]) {
i.value++;
break;
}
}
});
result.sort(function (a, b) {
// 特征弧线次数降序排列
return b.value - a.value;
});
// 算出出现次数最多的特征
return { result: result[0].label, dists: dists };
}
这是我在大神的注释下再添加一些注释。
然后,这是大神使用canvas将算法演示出来。代码(为了方便理解,我添加了自己的注释)如下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>KNN</title>
</head>
<body>
<canvas id="canv" style="border: 1px #000 solid;"></canvas>
<!-- 当前点 -->
<span id="point"></span>
<input type="color" id="clrDom" value="#80ff00" />
<input type="number" id="KKKKK" placeholder="3" />
<script src="KNN.js"></script>
<script>
var dataSet = [];
var drawMousePoint = false;
var canvas = document.getElementById("canv");
var clrDom = document.getElementById("clrDom");
var KKKKK = document.getElementById("KKKKK");
var cxt = canvas.getContext("2d");
var color = "#80ff00";
// 画布的长宽
canvas.width = 600;
canvas.height = 300;
function getEvPoint(e) {
// console.log(e);
return { x: e.offsetX, y: e.offsetY };
}
function onMouseOut(e) {
if (!drawMousePoint) return;
drawMousePoint = false;
}
function onMouseMove(e) {
// 鼠标移动事件调用函数draw,计算距离并排序,取其中最接近的k个
drawMousePoint = true;
clear();
draw(e);
}
function clickCanv(e) {
var p = getEvPoint(e);
dataSet.push({
point: p,
color: color
});
}
function draw(e) {
var p = getEvPoint(e);
var r = null;
if (dataSet.length) {
r = classify0(p, dataSet, 'point', 'color', parseInt(KKKKK.value) || 3,function(p1, p2){
//根据欧几里得距离公式或勾股定理计算距离
var d = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
return d;
});
// console.log(r);
}
if (e) {
cxt.beginPath();
cxt.arc(p.x, p.y, 8, 0, 2 * Math.PI, false);
// 定义最初点的颜色
cxt.fillStyle = r && r.result ? r.result : '#efefef';
// 连接点
cxt.stroke();
cxt.fill();
// 在页面上显示当前坐标
document.getElementById("point").innerHTML = "(" + p.x + "," + p.y + ")";
document.getElementById("point").style.color = cxt.fillStyle;
}
for (var i of dataSet) {
// 描绘训练集的点
cxt.beginPath();
cxt.lineWidth = 1;
cxt.arc(i.point.x, i.point.y, 4, 0, 2 * Math.PI, false);
cxt.fillStyle = i.color;
cxt.stroke();
cxt.fill();
}
if (r) {
// 连接点的线段
for (var i of r.dists) {
cxt.beginPath();
cxt.lineWidth = 1;
cxt.moveTo(p.x, p.y);
cxt.lineTo(i.p.point.x, i.p.point.y);
cxt.stroke();
cxt.fill();
}
}
}
function changeColor() {
color = this.value;
}
function clear() {
cxt.clearRect(0, 0, canvas.width, canvas.height);
}
canvas.addEventListener('click', clickCanv, false);
canvas.addEventListener('mousemove', onMouseMove, false);
canvas.addEventListener('mouseout', onMouseOut, false);
clrDom.addEventListener('change', changeColor, false);
</script>
</body>
</html>
对于大神的实现。我有两点改进的想法:1.将canvas单独提出来,方便复用。2.对于类别<= 5 , 总点数 <=20(类别过大,点数过多运算会很庞大)启用背景模式,即讲canvas上所有点的颜色(除了已经存在的外)赋予通过KNN算法所算出的算法颜色(透明度为0.5),显示出背景。但一个个算出来,运算量巨大还好像看起来很蠢。(优化:可以计算类别边缘,但这样的算法我不懂)
这个又是个大坑,待更新。