《機器學習實戰》——k近鄰算法

一、前言

如何區分一部片是愛情片還是動作片,愛情片中也有動作,動作片中也有親吻的鏡頭。但是,動作片中打鬥的場景更多,愛情片中親吻的鏡頭更多。所以,可以引入“統計概率”的概念,基於某類場景出現的次數來分類。

二、k近鄰算法概述

(1)原理:

給定一堆數據集,對於新輸入的測試樣例,通過計算其與數據集不同樣本特徵值之間的距離,從中找出與測試樣例最相近的k個鄰居。通過判斷這k個鄰居中哪種類別佔多數,則預測該測試樣例也屬於該類。

(2)優點:

精度高,對異常值不敏感(只與相近的k個值相關),無數據輸入假定

(3)缺點:

計算複雜度高,空間複雜度高,學習效率低(算不上學習,只是統計)

(4)特別注意:

其中,最重要的參數是K的選擇。

如下圖所示,輸入綠色圓圈爲測試樣例。當K=3時,根據原理,最近的3個鄰居中,紅色三角佔多數,則預測綠色圓圈屬於紅色三角類;當K=5時,同理,5個鄰居中,藍色方塊佔多數,則預測綠色圓圈屬於藍色方塊類。

因此,K的選擇將直接影響了預測的結果。如果K值過小,則對於鄰近的訓練數據特別敏感,容易學習到噪點數據,而造成過擬合;但是如果K值過大,把所有數據都當做鄰居來預測,那就減弱了學習能力,只是簡單地統計數值而已。

因此,根據李航的《統計學習方法》書中所說,K的選擇,應該先以一個小值開始,利用交叉驗證的方法,通過不斷調參來選取最優值。通常k是不大於20的整數。

在這裏插入圖片描述
其次,對於距離公式的選擇,一般是選擇歐式距離,若p(p1,p2,…,pn)和q(q1,q2,…,qn)是空間的兩點,則他們的距離爲:
在這裏插入圖片描述

三、實踐出真知

(1)核心算法

創建一個kNN.py文件,根據以上原理,寫出核心算法代碼。

def kNN_classify(test_datas, train_data_set, labels, k):
    """ k近鄰分類算法 """
    data_set_size = train_data_set.shape[0] # 數據集個數,行數
    # 1、計算新數據與訓練集的歐氏距離
    # tile是將測試集複製成與訓練集同行數,一列
    diff_mat = tile(test_datas, (data_set_size, 1)) - train_data_set #(x1-x2)
    sq_diff_mat = diff_mat ** 2 #(x1-x2)平方
    sq_distances = sq_diff_mat.sum(axis=1) # 按行累加
    distances = sq_distances ** 0.5 # 開根號算出歐式距離
    # 2、按照距離遞增排序
    sorted_dist_indicies = distances.argsort()
    # 3、選取距離最近的前 k個,計算類別個數
    class_count = {} # key是類別,value是個數
    for i in range(k):
        vote_label = labels[sorted_dist_indicies[i]]
        # 這裏字典的get函數是將找key爲vote_label的值,若找不到,則用0
        class_count[vote_label] = class_count.get(vote_label, 0) + 1 # 計算類別個數
    # 4、排序找出類別個數最高的作爲預測結果
    sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), 
                        reverse=True) # 按照value(類的個數)逆序排序,找出類別最多的
    return sorted_class_count[0][0] # [0][0]是預測的標籤,[0][1]爲k中最多的類別數

完整的驗證kNN算法的程序文件爲:

from numpy import *
import operator

def create_dataset():
    """ 生成數據集 """
    train_datas = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
    labels = ['動作片', '動作片', '愛情片', '愛情片']
    return train_datas, labels

def kNN_classify(test_datas, train_data_set, labels, k):
    """ k近鄰分類算法 """
    data_set_size = train_data_set.shape[0] # 數據集個數,行數
    # 1、計算新數據與訓練集的歐氏距離
    # tile是將測試集複製成與訓練集同行數,一列
    diff_mat = tile(test_datas, (data_set_size, 1)) - train_data_set #(x1-x2)
    sq_diff_mat = diff_mat ** 2 #(x1-x2)平方
    sq_distances = sq_diff_mat.sum(axis=1) # 按行累加
    distances = sq_distances ** 0.5 # 開根號算出歐式距離
    # 2、按照距離遞增排序
    sorted_dist_indicies = distances.argsort()
    # 3、選取距離最近的前 k個,計算類別個數
    class_count = {} # key是類別,value是個數
    for i in range(k):
        vote_label = labels[sorted_dist_indicies[i]]
        # 這裏字典的get函數是將找key爲vote_label的值,若找不到,則用0
        class_count[vote_label] = class_count.get(vote_label, 0) + 1 # 計算類別個數
    # 4、排序找出類別個數最高的作爲預測結果
    sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), 
                        reverse=True) # 按照value(類的個數)逆序排序,找出類別最多的
    return sorted_class_count[0][0] # [0][0]是預測的標籤,[0][1]爲k中最多的類別數

if __name__ == '__main__':
    train_datas, labels = create_dataset()
    test_1 = [1.0, 0.9]
    pred_class = kNN_classify(test_1, train_datas, labels, 3)
    print("test_1預測爲:", pred_class)
    test_2 = [0.2, 0.2]
    pred_class = kNN_classify(test_2, train_datas, labels, 3)
    print("test_2預測爲:", pred_class)

結果爲:

test_1預測爲: 動作片
test_2預測爲: 愛情片

(2)約會人員喜好預測

註釋已經很詳細,所以簡單粗暴上代碼:

from numpy import *
from kNN import kNN_classify
import matplotlib.pyplot as plt
import sys

def file_to_dataset(filename):
    """ 將文件的數據集轉換成分類器的輸入格式 """
    try:
        with open(filename) as f_obj:
            # 一次性讀整個文件,自動將內容分析成一個行的字符串列表
            list_of_lines = f_obj.readlines()
    except FileNotFoundError:
        print("Sorry, the file " + filename + " does not exist.")
        sys.exit()
    numbers_of_lines = len(list_of_lines) # 獲取文件行數
    dataset = zeros((numbers_of_lines, 3))
    labels = []
    index = 0
    for line in list_of_lines:
        line = line.strip() # 去掉頭尾的指定字符,默認是空格或換行符
        list_from_line = line.split('\t') # 以'\t'分割行,形成列表 
        # 取前三列放進矩陣中, matrix[index, :]表示index行所有列
        dataset[index, :] = list_from_line[0:3]
        labels.append(int(list_from_line[-1])) # 取最後一列作爲標籤放進標籤列表
        index += 1
    return dataset, labels

def show_dataset(dataset, labels):
    """ 可視化數據 """
    fig = plt.figure() # 創建一個畫板對象
    ax = fig.add_subplot(111) # 創建子畫板,位於第一行第一列第一個位置
    colors = []
    for i in labels:
        if i == 1:
            colors.append('red')
        elif i== 2:
            colors.append('green')
        else :
            colors.append('blue')
    ax.scatter(x=dataset[:, 0], y=dataset[:, 1], 
            c=colors, s=15) # 取第2、3列作爲x、y畫散列圖
    plt.show()

def normalization(dataset):
    """ 
    將數據集歸一化到(0, 1)之間 
    new_value = (old_value-min)/(max-min)
    """
    min_values = dataset.min(0) # 參數0確保取該矩陣每一列的最小值,1是每一行的最小值
    max_values = dataset.max(0)
    ranges = max_values - min_values
    norm_dateset = zeros(shape(dataset))
    num_of_lines = dataset.shape[0]
    # tile是複製功能
    norm_dateset = dataset - tile(min_values, (num_of_lines, 1)) 
    norm_dateset = norm_dateset / tile(ranges, (num_of_lines, 1))
    return norm_dateset, ranges, min_values

def dating_test():
    """ 測試分類器效率 """
    ratio = 0.10 # 測試集佔 10%
    train_datas, labels = file_to_dataset('ch02_kNN\datingTestSet2.txt')
    norm_dataset, ranges, min_values = normalization(train_datas)
    num_of_lines = norm_dataset.shape[0] # 總行數
    num_test_data = int(num_of_lines * ratio) # 測試行數
    error_count = 0.0
    for i in range(num_test_data):
        res = kNN_classify(norm_dataset[i, :], 
            norm_dataset[num_test_data: num_of_lines, :],
            labels[num_test_data: num_of_lines], 4)
        print("the classifier prediction is: " + str(res) + 
            ", the real answer is:" + str(labels[i]))
        if res != labels[i] :
            error_count += 1.0
    print("Error rate : " + str((error_count/float(num_test_data)*100)) + "%")
    print("Accuracy rate : "+str((1-(error_count/float(num_test_data)))*100)+"%")

def classify_person():
    """ 輸出完整結果 """
    res_lists = ['not at all', 'in small doses', 'in large doses']
    percent_tats = float(input("percentage of time spend playing vodeo games?")) 
    ff_miles = float(input("frequent flier miles earned per year?"))
    ice_cream = float(input("liters of ice cream consumed per year?"))
    train_datas, labels = file_to_dataset('ch02_kNN\datingTestSet2.txt')
    norm_dataset, ranges, min_values = normalization(train_datas)
    new_test = array([ff_miles, percent_tats, ice_cream])
    new_test = (new_test-min_values)/ranges # 對新數據歸一化
    res = kNN_classify(new_test, norm_dataset, labels, 4)
    print("You will probably like this person: ", res_lists[res-1])

if __name__ == '__main__':
    '''
    # 準備數據
    train_datas, labels = file_to_dataset('ch02_kNN\datingTestSet2.txt')
    print("train_datas:\n", train_datas)
    print("labels:\n", labels[0:20])
    # 可視化
    show_dataset(train_datas, labels)
    # 歸一化
    norm_dataset, ranges, min_values = normalization(train_datas)
    print("歸一化後數據集:\n", norm_dataset)
    print("每列的數值範圍:\n", ranges)
    print("每列最小值:\n", min_values)
    # 測試
    dating_test()
    '''
    # 使用
    classify_person()
    

結果爲:

percentage of time spend playing vodeo games?10
frequent flier miles earned per year?10000
liters of ice cream consumed per year?0.5
You will probably like this person:  in small doses

(3)手寫數字識別

簡單粗暴上代碼:

from numpy import *
from kNN import kNN_classify
import sys, os

def img_to_vector(filename):
    """ 將一張數字圖像轉成向量格式 """
    digit_vector = zeros((1, 1024))
    try:
        with open(filename) as f_obj:
            for i in range(32):
                list_of_line = f_obj.readline()
                for j in range(32):
                    digit_vector[0, 32*i+j] = int(list_of_line[j])
            return digit_vector
    except FileNotFoundError:
        print("Sorry, the file " + filename + " does not exist.")
        sys.exit() 

def create_train_set():
    """ 將文件數據轉爲輸入格式 """
    labels = [] # 存放訓練集的標籤
    train_file_list = os.listdir(r"ch02_KNN\digits\trainingDigits") # 得到所有文件名
    num_train_set = len(train_file_list) # 訓練的圖片個數
    train_datas = zeros((num_train_set, 1024)) # 創建訓練集矩陣
    # 遍歷處理每一張圖片
    for i in range(num_train_set):
        full_file_name = train_file_list[i] # 0_13.txt
        file_name = full_file_name.split('.')[0] # 以‘。’分割,取第一個,即:0_13
        label = int(file_name.split('_')[0]) # 以‘_’分割,取第一個,0就是標籤
        labels.append(label)
        train_datas[i,:] = img_to_vector(
            r"ch02_KNN\digits\trainingDigits\%s" % full_file_name 
        )
    return train_datas, labels

def hand_writing_test():
    """ 使用kNN算法識別手寫數字 """
    # 獲取訓練集
    train_datas, labels = create_train_set()
    # 處理測試集
    test_file_list = os.listdir(r"ch02_KNN\digits\testDigits")
    error_count = 0.0 # 統計錯誤率
    num_test_set = len(test_file_list)
    for i in range(num_test_set):
        full_file_name = test_file_list[i]
        file_name = full_file_name.split('.')[0]
        label = int(file_name.split('_')[0])
        test_data = img_to_vector(
            r"ch02_KNN\digits\testDigits\%s" % full_file_name
        )
        # 使用 kNN分類器分類
        pred_res = kNN_classify(test_data, train_datas, labels, 3)
        print("the classifier prediction is: " + str(pred_res) + 
            ", the real answer is:" + str(label))
        if pred_res != label:
            error_count += 1.0
    print("Error rate : {:.2f}%".format(error_count/float(num_test_set)*100))
    print("Accuracy rate : {:.2f}%".format((1-(error_count/float(num_test_set)))
                                            *100))

if __name__ == "__main__":
    hand_writing_test()

結果爲:

the classifier prediction is: 0, the real answer is:0
the classifier prediction is: 0, the real answer is:0
the classifier prediction is: 0, the real answer is:0
。。。
the classifier prediction is: 9, the real answer is:9
the classifier prediction is: 9, the real answer is:9
the classifier prediction is: 9, the real answer is:9
the classifier prediction is: 9, the real answer is:9
Error rate : 1.06%
Accuracy rate : 98.94%

以上,就是這個算法的介紹和實踐,有錯誤的希望留言指正,有疑問的也歡迎留言一起交流學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章