机器学习-4 k-NN【附代码】

返回主页


k 最近邻算法(k-nearest neighbor, k-NN)是一种基本的分类方法(二分类 / 多分类),由 Cover 和 Hart 于1968年提出。它的输入为特征向量,通过距离度量多数表决,输出类别标记。该算法不具有显式的学习过程,且 k 值的选择对结果会产生重大的影响,因此对于 k-NN 而言,k 的选择至关重要

1、定义数据集

数据集
特征空间

2、假设空间

每个样本的预测类别 = 该样本所属的以 k 为范围的邻域内所有训练样本类别的投票值

关于距离的度量一般有曼哈顿距离欧氏距离等等:

3、目标函数

目标函数的任务是使所有训练样本的邻域内误分类率最低,所以 k 是待估计的参数

4、优化算法
k-NN 不具有显式的学习过程,一般在预测的过程中通过线性扫描KD树的方法执行判断

手写 k-NN 算法并与 Sklearn 作比较

# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
import numpy as np
import pandas as pd
import scipy.spatial.distance as dist
from sklearn.utils import shuffle
from sklearn.model_selection import train_test_split
#import random as rd
#import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier


class KnnModel(object):
    @staticmethod
    def check_k(K, x_train):
        if not isinstance(K, np.ndarray):
            raise TypeError("K must be as type of np.ndarray")
            
        if (min(K) <= 0) or (max(K) > len(x_train)):
            raise ValueError("K must be in range [1, N]")
            
        for k in K:
            if not isinstance(k, np.int32):
                raise TypeError("the element of K must be as type of np.int32")
        return None
        
    
    def __init__(self, K, metric):
        self.K = K
        self.metric = metric
    
    
    def z_scale(self, x_train):
        '''z标准化,在动用距离度量的算法中,必须先进行标准化以消除数据量纲的影响'''
        mu = np.mean(x_train, axis=0)
        std = np.std(x_train, axis=0)
        return mu, std
    
    
    def data_transform(self, mu, std, x_train, x_test):
        '''数据变换,执行标准化操作'''
        x_train_scale = (x_train - mu) / std
        x_test_scale = (x_test - mu) / std
        return x_train_scale, x_test_scale
    
    
    def l1(self, x1, x2):
        '''曼哈顿距离'''
        return np.sum(np.abs(x1 - x2))
    
    
    def l2(self, x1, x2):
        '''欧氏距离'''
        return np.sqrt(np.sum((x1 - x2)**2))
    
    
    def cosine(self, x1, x2):
        '''余弦距离'''
        return 1 - x1.dot(x2) / (np.sqrt(np.sum(x1**2)) * np.sqrt(np.sum(x2**2)))
    
    
    def fit(self, x_train_scale, y_train):
        '''模型训练,针对KNN,其实没有显示的学习过程,所谓的训练其主要目的就是选择合适的k值'''
        # 训练样本量
        N = len(x_train_scale)
        # 适配距离函数
        if self.metric == "l1":
            m = self.l1
        elif self.metric == "l2":
            m = self.l2
        elif self.metric == "cosine":
            m = self.cosine
        else:
            raise ValueError("metric must be 'l1', 'l2' or 'cosine'")
        # 计算样本两两距离,形成距离方阵
        #dist_train = dist.cdist(x_train_scale, x_train_scale, metric="euclidean")
        dist_train = dist.cdist(x_train_scale, x_train_scale, metric=m)
        dist_train = pd.DataFrame(dist_train)
        # 迭代,选择最优的k值
        loss_list = []
        for k in K:
            loss_res = 0
            for idx in dist_train.index:
                # 为每个样本计算k近邻索引
                k_nearest_idx = dist_train.iloc[idx, :].sort_values(ascending=True)[1:k+1].index
                # 得到k近邻的类别标记
                c = y_train[k_nearest_idx]
                # 计算误分类率
                loss = sum(y_train[idx] != c) / k
                loss_res += loss
            # 总体误分类率
            loss_res /= N
            loss_list.append(loss_res)
        # 取误分类率最小的k为最优值
        k_best = K[np.argmin(loss_list)]
        print(f"误分类率列表:\n{loss_list} \n最优的k:{k_best}")
        return k_best
    
    
    def predict(self, x_train_scale, x_test_scale, y_train, k_best):
        '''模型预测,每个样本的预测类别 = 该样本所属的以 k 为范围的邻域内所有训练样本类别的投票值'''
        # 适配距离函数
        if self.metric == "l1":
            m = self.l1
        elif self.metric == "l2":
            m = self.l2
        elif self.metric == "cosine":
            m = self.cosine
        else:
            raise ValueError("metric must be 'l1', 'l2' or 'cosine'")
        # 计算测试集样本与训练集样本的两两距离
        dist_test = dist.cdist(x_test_scale, x_train_scale, metric=m)
        dist_test = pd.DataFrame(dist_test)
        # 执行预测,找到以 k 为范围的邻域内所有训练样本类别的投票值
        y_pred = []
        for i in range(len(dist_test)):
            k_nearest_idx = dist_test.iloc[i, :].sort_values(ascending=True)[0:k_best].index
            c = y_train[k_nearest_idx]
            label, label_count = np.unique(c, return_counts=True)
            y_hat = label[np.argmax(label_count)]
            y_pred.append(y_hat)
        return y_pred
    
    
    def get_score(self, y_true, y_pred):
        '''模型评估'''
        score = sum(y_true == y_pred) / len(y_true)
        return score
        

if __name__ == "__main__":
    # 构造多分类数据集
    n1 = 20
    x1 = np.random.uniform(low=1, high=5, size=[n1, 4]) + np.random.randn(n1, 4)*0.01
    y1 = np.tile(0, n1)
    
    n2 = 10
    x2 = np.random.uniform(low=6, high=10, size=[n2, 4]) + np.random.randn(n2, 4)*0.01
    y2 = np.tile(1, n2)
    
    n3 = 30
    x3 = np.random.uniform(low=8, high=20, size=[n3, 4]) + np.random.randn(n3, 4)*0.01
    y3 = np.tile(2, n3)
    
    x = np.concatenate([x1, x2, x3], axis=0)
    y = np.concatenate([y1, y2, y3])
    
    x, y = shuffle(x, y, random_state=0)
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
    
    K = np.array([3, 5, 7, 9, 11, 15, 20]).astype(np.int32)
    
    # 手写 k-NN
    model = KnnModel(K=K, metric="l2")
    model.check_k(K, x_train)
    
    mu, std = model.z_scale(x_train)
    x_train_scale, x_test_scale = model.data_transform(mu, std, x_train, x_test)
    k_best = model.fit(x_train_scale, y_train)
    
    y_pred = model.predict(x_train_scale, x_test_scale, y_train, k_best)
    score = model.get_score(y_test, y_pred)
    print(f"KnnModel 预测准确率:{score}")
    
    # sklearn
    scale = StandardScaler(with_mean=True, with_std=True)
    scale.fit(x_train)
    x_train_scale = scale.transform(x_train)
    x_test_scale = scale.transform(x_test)
    
    clf = KNeighborsClassifier(n_neighbors=5, algorithm="kd_tree", metric="euclidean")
    clf.fit(x_train_scale, y_train)
    y_pred = clf.predict(x_test_scale)
    score = sum(y_test == y_pred) / len(y_test)
    print(f"KnnSklearn 预测准确率:{score}")

误分类率列表:
[0.020833333333333332, 0.024999999999999998, 0.029761904761904757, 0.05324074074074075, 0.0890151515151515, 0.14444444444444446, 0.25]
最优的k:3
KnnModel 预测准确率:1.0

KnnSklearn 预测准确率:1.0


返回主页

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容