源於數據挖掘的一個做業, 這裏用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...