1. 程式人生 > >k近鄰分類(KNN)

k近鄰分類(KNN)

    近來學習了最近鄰分類KNN(k Nearest Neighbors),寫下心得,以為記錄。

    K近鄰的原理很簡單,對於資料集,可以分為訓練集和測試集,KNN使用訓練集全部資料作為分類計算的依據。輸入一個待分類樣本,計算該樣本與訓練集所有樣本的距離,挑選出距離最小的k個樣本,統計這k個樣本的類別標籤,出現最多的那個類別就被認為是待分類樣本的類別。

    《機器學習實戰》中使用歐式距離作為特徵向量距離的度量,其他距離也可以用,不說。

    跟其他很多分類器一樣,由於特徵向量每個維度資料範圍不一樣,某個維度過大,會嚴重影響距離計算,因此要進行歸一化。

    程式碼本身很簡單,只是對Python和Python相關一系列的庫還不是很熟悉,因此花了點時間摸索。用KNN來做數字識別,訓練樣本2000個,測試樣本946,k取3,最終準確率為98.8%,還可以,就是計算時間很長,計算時間與訓練樣本的數量是線性關係,這或許就是KNN的一個缺點,SVM就只需要留下支撐向量。計算時間還與k有關係,準確率也和k有關,k取10準確率就只有97.7了,這也許能說明什麼。

    程式碼中還有約會網站分類問題的程式碼。資料集可以網上下載到,不傳了。

from pandas import Series, DataFrame
import numpy as np
import seaborn as sns
from matplotlib import pylab as plt
from os import listdir


# _dataset的shape[0]和labels的數量一樣
# DataFrame的index預設是range(n),後面用這一特性來查詢排序後標籤的位置
def create_dataset():
    _dataset = DataFrame([[1., 1.1],
                         [1., 1.],
                         [0., 0.],
                         [0.1, 0.1]])
    _label = Series(["A", "A", "B", "B"])
    return _dataset, _label


def create_dataset1():
    fr = open("datingTestSet.txt", "r")
    d = fr.readlines()
    li = []
    for line in d:
        li.append(line.strip().split())
    _ds = DataFrame(li, columns=["flight", "gamepercent", "icecream", "labels"])
    _label = _ds["labels"]
    del _ds["labels"]
    _ds = _ds.astype(float)  #

    # print(_ds)
    # print(set(_label))
    # print(_ds.shape)
    return _ds, _label


# 讀取文字形式的影象,轉換為一維向量並返回
def img2vector(filepathname):
    fr = open(filepathname, "r")
    vector = np.zeros((1, 1024))
    for i in range(32):
        line = fr.readline()
        for j in range(32):
            vector[0, i*32 + j] = int(line[j])
    return vector


# 讀取數字識別的資料集
def create_dataset2(filepath):
    dir_list = listdir(filepath)
    img_n = len(dir_list)
    _labels = Series(index=range(img_n))
    _dataset_list = np.zeros((img_n, 1024))

    for i in range(img_n):
        line = dir_list[i]
        _name = line.split(".")[0]
        _digit_lab = _name.split("_")[0]
        _labels[i] = int(_digit_lab)
        _dataset_list[i, :] = img2vector(filepath + "\\" + line)
    _dataset = DataFrame(_dataset_list, columns=range(1024))
    print(_labels.shape)
    print(_dataset.shape)

    return _dataset, _labels


# 將ratio的資料劃分為訓練資料,其餘作為測試資料
def split_dataset(_org_dataset, _org_labels, train_ratio):
    _train_n = int(_org_dataset.shape[0] * train_ratio)
    train_ds = _org_dataset[:][0:_train_n]
    train_lb = _org_labels[0:_train_n]
    test_ds = _org_dataset[:][_org_dataset.index > _train_n]
    test_lb = _org_labels[_org_labels.index > _train_n]
    return train_ds, train_lb, test_ds, test_lb


def knn_classify(dataset, labels, test_feature, k, is_normalize=False):
    if k > dataset.shape[0]:
        print("k should not large than the number of sample")
        return None
    # 計算歐式距離
    norm_para = []
    test_feature.index = dataset.columns
    norm_ds = DataFrame(columns=dataset.columns, index=dataset.index)
    norm_ts_f = Series(index=dataset.columns)
    # 歸一化很浪費時間,本應該把dataset的歸一化放在外面,這裡作為測試,懶得改了
    if is_normalize:
        for c in dataset:
            norm_para.append([dataset[c].min(), dataset[c].max()])
        # print(norm_para)
        for c in range(dataset.shape[1]):
            norm_ds[norm_ds.columns[c]] = (dataset[dataset.columns[c]] - norm_para[c][0])/(norm_para[c][1] - norm_para[c][0] + 0.000001)
        for c in range(test_feature.shape[0]):
            norm_ts_f[dataset.columns[c]] = (test_feature.values[c] - norm_para[c][0])/(norm_para[c][1] - norm_para[c][0] + 0.000001)
    else:
        norm_ds = dataset.copy()
        norm_ts_f = test_feature.copy()

    d = norm_ds - norm_ts_f
    # print(d[0:2])
    d = d**2
    # print(d[0:2])
    d = d.sum(axis=1)
    d = d**0.5
    # print(d[0:2])
    # 對歐式距離進行降序排序,注意這裡用的是sort_values
    # 資料的index也會跟隨資料的位置發生改變,也就是用鍵值對訪問d,d沒有發生任何變化
    d = d.sort_values(ascending=True)
    # combine 僅僅是為了除錯顯示使用
    # combine = np.array([[d.index[i], d.values[i], labels[d.index[i]]] for i in range(len(d))])
    # print("k nearest neighbor is:")
    # for i in range(k):
    #     print(combine[i])
    # print(d)
    # 用set來保證類別標籤的唯一性,利用為唯一標籤構建Series,統計前k個特徵類別出現的次數
    # 然後對k個特徵的類別出現次數進行排序,返回出現最多的那一個類別標籤
    unique_labels = set(labels)
    labels_number = unique_labels.__len__()
    knn_set = Series(np.zeros(labels_number), unique_labels)
    # print("set for knn:")
    for i in range(k):
        # d.index是以列表訪問d的index,與現實的順序一樣
        # labels[d.index[i]]其實就是獲取對應特徵的類別標籤而已
        knn_set[labels[d.index[i]]] += 1

    knn_set = knn_set.sort_values(ascending=False)
    # print(knn_set)
    # print(knn_set)
    return knn_set.index[0]


def classify_dataset(dataset, labels, test_ds, test_lb, k, is_normalize):
    res = []
    n = test_ds.shape[0]

    # show_ds = dataset.copy()
    # show_ds["labels"] = labels

    # sns.pairplot(show_ds, hue="labels")
    # plt.show()

    for i in range(n):
        print("classify " + str(i) + ":")
        fe = Series(test_ds.values[i], index=dataset.columns)
        ans = knn_classify(dataset, labels, fe, k, is_normalize)
        res.append(ans)
        if ans != test_lb.values[i]:
            print("error classify:" + str(test_ds.index[i]))
    right_n = 0
    # print("res:")
    for i in range(n):
        # print(str(test_lb.values[i]) + " " + str(res[i]))
        if res[i] == test_lb.values[i]:
            right_n += 1
    acc = right_n * 1.0 / n
    print("accurracy=" + str(acc))


# 測試約會網站分類
# org_dataset, org_label = create_dataset1()
# train_dataset, train_label, test_dataset, test_label = split_dataset(org_dataset, org_label, 0.8)
# classify_dataset(train_dataset, train_label, test_dataset, test_label, 10, True)


# 測試數字識別
train_dataset, train_label = create_dataset2("digits\\trainingDigits")
test_dataset, test_label = create_dataset2("digits\\testDigits")
classify_dataset(train_dataset, train_label, test_dataset, test_label, 3, False)