機(jī)器學(xué)習(xí)-4 k-NN【附代碼】

返回主頁(yè)


k 最近鄰算法(k-nearest neighbor, k-NN)是一種基本的分類方法(二分類 / 多分類),由 Cover 和 Hart 于1968年提出。它的輸入為特征向量,通過(guò)距離度量多數(shù)表決,輸出類別標(biāo)記。該算法不具有顯式的學(xué)習(xí)過(guò)程,且 k 值的選擇對(duì)結(jié)果會(huì)產(chǎn)生重大的影響,因此對(duì)于 k-NN 而言,k 的選擇至關(guān)重要。

1、定義數(shù)據(jù)集

數(shù)據(jù)集
特征空間

2、假設(shè)空間

每個(gè)樣本的預(yù)測(cè)類別 = 該樣本所屬的以 k 為范圍的鄰域內(nèi)所有訓(xùn)練樣本類別的投票值

關(guān)于距離的度量一般有曼哈頓距離歐氏距離等等:

3、目標(biāo)函數(shù)

目標(biāo)函數(shù)的任務(wù)是使所有訓(xùn)練樣本的鄰域內(nèi)誤分類率最低,所以 k 是待估計(jì)的參數(shù)

4、優(yōu)化算法
k-NN 不具有顯式的學(xué)習(xí)過(guò)程,一般在預(yù)測(cè)的過(guò)程中通過(guò)線性掃描KD樹的方法執(zhí)行判斷

手寫 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標(biāo)準(zhǔn)化,在動(dòng)用距離度量的算法中,必須先進(jìn)行標(biāo)準(zhǔn)化以消除數(shù)據(jù)量綱的影響'''
        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):
        '''數(shù)據(jù)變換,執(zhí)行標(biāo)準(zhǔn)化操作'''
        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):
        '''模型訓(xùn)練,針對(duì)KNN,其實(shí)沒有顯示的學(xué)習(xí)過(guò)程,所謂的訓(xùn)練其主要目的就是選擇合適的k值'''
        # 訓(xùn)練樣本量
        N = len(x_train_scale)
        # 適配距離函數(shù)
        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'")
        # 計(jì)算樣本兩兩距離,形成距離方陣
        #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)
        # 迭代,選擇最優(yōu)的k值
        loss_list = []
        for k in K:
            loss_res = 0
            for idx in dist_train.index:
                # 為每個(gè)樣本計(jì)算k近鄰索引
                k_nearest_idx = dist_train.iloc[idx, :].sort_values(ascending=True)[1:k+1].index
                # 得到k近鄰的類別標(biāo)記
                c = y_train[k_nearest_idx]
                # 計(jì)算誤分類率
                loss = sum(y_train[idx] != c) / k
                loss_res += loss
            # 總體誤分類率
            loss_res /= N
            loss_list.append(loss_res)
        # 取誤分類率最小的k為最優(yōu)值
        k_best = K[np.argmin(loss_list)]
        print(f"誤分類率列表:\n{loss_list} \n最優(yōu)的k:{k_best}")
        return k_best
    
    
    def predict(self, x_train_scale, x_test_scale, y_train, k_best):
        '''模型預(yù)測(cè),每個(gè)樣本的預(yù)測(cè)類別 = 該樣本所屬的以 k 為范圍的鄰域內(nèi)所有訓(xùn)練樣本類別的投票值'''
        # 適配距離函數(shù)
        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'")
        # 計(jì)算測(cè)試集樣本與訓(xùn)練集樣本的兩兩距離
        dist_test = dist.cdist(x_test_scale, x_train_scale, metric=m)
        dist_test = pd.DataFrame(dist_test)
        # 執(zhí)行預(yù)測(cè),找到以 k 為范圍的鄰域內(nèi)所有訓(xùn)練樣本類別的投票值
        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):
        '''模型評(píng)估'''
        score = sum(y_true == y_pred) / len(y_true)
        return score
        

if __name__ == "__main__":
    # 構(gòu)造多分類數(shù)據(jù)集
    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 預(yù)測(cè)準(zhǔn)確率:{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 預(yù)測(cè)準(zhǔn)確率:{score}")

誤分類率列表:
[0.020833333333333332, 0.024999999999999998, 0.029761904761904757, 0.05324074074074075, 0.0890151515151515, 0.14444444444444446, 0.25]
最優(yōu)的k:3
KnnModel 預(yù)測(cè)準(zhǔn)確率:1.0

KnnSklearn 預(yù)測(cè)準(zhǔn)確率:1.0


返回主頁(yè)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容