機器學習實戰筆記(1)——kNN(k Nearest Neighbor)算法

簡述

kNN算法(中文翻譯:k-近鄰算法)是機器學習分類算法的基礎部分,也是比較簡單的算法之一。它的內容和原理並不複雜,但是計算量比較大,即時間複雜度空間複雜度都比較高。書中以約會網站和手寫數字識別系統爲例。在這裏,筆者也將從這兩個例子下手,但是對部分代碼進行了改進,以便適應Python3的編程環境。

算法描述

kNN的k指的是在新數據與樣本數據進行比對時,只選取前k個最相近的數據。
kNN算法就是對未知類別屬性的數據集中的每個點依次執行以下操作:

  1. 計算已知類別數據集中的點與當前點之間的距離(歐氏距離:
    d=(xAxB)2+(yAyB)2

    );
  2. 按照距離遞增次序排序;
  3. 選取與當前點距離最小的k個點;
  4. 確定前k個點所在類別的出現頻率;
  5. 返回前k個點出現頻率最高的類別作爲當前點的預測分類。

特點

  • 優點:精度高,對異常值不敏感,無數據輸入假定。
  • 缺點:計算複雜度較高,空間複雜度較高。
  • 適用範圍:數值型和標稱型。數據需帶有目標數據,即人工標籤。標籤形式可以是文件名,也可以是文檔內的某一列。

算法處理一般流程

  1. 收集數據
  2. 準備數據:結構化的數據格式,有自己的數據格式即可。
  3. 分析數據
  4. 訓練算法:此步驟不適用於kNN,但是爲了明確一般流程,仍然加上。
  5. 測試算法:計算錯誤率。
  6. 使用算法:首先輸入樣本數據和結構化的輸出結果,然後運行kNN算法判定數據分別屬於哪一個分類,最後應用於分類的後續處理。

module

from numpy import *     # numpy matrix and array process
import operator         # sorted() function's 'key'parameter
from os import listdir  # used to list the folder files

收集、解析數據

以文本文件的數據爲例,提取其中的矩陣數據(一般以二維數據居多)和標籤信息。

def file2matrix(filename):
    """
    txt file data change to matrix
    @param filename: filename
    @return: the read_matrix and the labels
    """
    with open(filename, mode='r') as fr:
        array_lines = fr.readlines()
    number_of_lines = len(array_lines)
    return_mat = zeros((number_of_lines, 3))
    class_label_vector = []
    index = 0
    for line in array_lines:
        line = line.strip()
        list_from_line = line.split('\t')
        return_mat[index, :] = list_from_line[0:3]
        class_label_vector.append(int(list_from_line[-1]))
        index += 1
    return return_mat, class_label_vector

算法核心代碼實現

def classify0(inX, dataset, labels, k):
    """
    knn classify
    @param inX: the input vector which is ready to be classified
    @param dataset: the training data set
    @param labels: labels vector
    @param k: the k-th
    @return: sorted result
    """
    dataset_size = dataset.shape[0]     # calculate the number of lines
    diff_mat = tile(inX, (dataset_size, 1)) - dataset
    sq_diff_mat = diff_mat**2
    sq_distances = sq_diff_mat.sum(axis=1)
    distances = sq_distances**0.5

    sorted_distance_indices = distances.argsort()
    class_count = {}
    for i in range(k):
        vote_i_label = labels[sorted_distance_indices[i]]
        class_count[vote_i_label] = class_count.get(vote_i_label, 0) + 1
    sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True)
    return sorted_class_count[0][0]

示例1:約會數據的分類

女主角Helen要在自己打了標籤的數據裏面得到一個模型,用以判斷今後遇到的男生對她的魅力值和吸引力。
我們先來看一部分她打過標籤的格式化數據:

40920   8.326976    0.953952    3
14488   7.153469    1.673904    2
26052   1.441871    0.805124    1
75136   13.147394   0.428964    1
38344   1.669788    0.134296    1
72993   10.141740   1.032955    1
35948   6.830792    1.213192    3
42666   13.276369   0.543880    3
67497   8.631577    0.749278    1
35483   12.273169   1.508053    3
50242   3.723498    0.831917    1
63275   8.385879    1.669485    1
5569    4.875435    0.728658    2
51052   4.680098    0.625224    1

從左至右依次是年飛行里程數、玩兒視頻遊戲所耗時間的百分比、每週消耗的冰激凌公升數以及最後的標籤(1-3依次是不喜歡、喜歡和非常喜歡)PS. 話說貌似打會兒遊戲還是很受歡迎的哈~
可將其繪製爲“冰激凌-遊戲時間圖”如下:
dataset distribution figure

準備數據:歸一化

歸一化就是把數據範圍限制在某個明確的範圍之內,比如接下來我們就需要把數據統一到(0,1)範圍內,方便後續的數據處理。代碼如下:

def autonorm(dataset):
    """
    dataset normalization
    @param dataset: np.array
    @return: dataset after norm, ranges, minimal value
    """
    min_val = dataset.min(0)
    max_val = dataset.max(0)
    ranges = max_val - min_val
    norm_dataset = zeros(shape(dataset))
    m = dataset.shape[0]
    norm_dataset = dataset - tile(min_val, (m, 1))
    norm_dataset = norm_dataset/tile(ranges, (m, 1))
    return norm_dataset, ranges, min_val

測試算法

編寫針對此示例的算法測試代碼:

def dating_class_test():
    """
    dating data test and see the error ratio
    @return: the output on screen which shows the result and the error rate
    """
    ho_ratio = 0.1  # the ratio of test data
    dating_data_mat, dating_labels = file2matrix('datingTestSet2.txt')
    norm_mat, ranges, min_val = autonorm(dating_data_mat)
    m = norm_mat.shape[0]
    num_test_vec = int(m*ho_ratio)
    error_count = 0.0
    for i in range(num_test_vec):
        # large scale data is used to be trained and small data is used to be test. 0:num_test_vec is small and
        # num_test_vec:m is large
        classify_result = classify0(norm_mat[i, :], norm_mat[num_test_vec:m, :], dating_labels[num_test_vec:m], 3)
        print("the classifier came back with: %d, the real answer is %d" % (classify_result, dating_labels[i]))
        if classify_result != dating_labels[i]:
            error_count += 1.0
    print("the total error rate is: %f%%" % (error_count/float(num_test_vec)*100.0))

代碼結果演示如下:
result1_1

使用算法

將此算法應用於具體的應用之內,根據一個人的三個標籤特徵判斷他對Helen的吸引力程度:

def classify_person():
    """
    Test the charm of a person to you
    @return: print the result
    """
    result_list = ['not at all', 'in small doses', 'in large doses']
    percent_games = float(input('Percentage of time spent playing video games: '))
    length_miles = float(input('Frequent flier miles earned per year: '))
    ice_cream = float(input('Liters of ice cream consumed per year: '))

    dating_data_mat, dating_labels = file2matrix('datingTestSet2.txt')
    norm_mat, ranges, min_val = autonorm(dating_data_mat)
    in_arr = array([length_miles, percent_games, ice_cream])
    class_fier_result = classify0((in_arr - min_val)/ranges, norm_mat, dating_labels, 3)
    print('You will probably like this person: ', result_list[class_fier_result-1])

算法運行結果如下:
result1_2

怎麼樣,你是否也能捕獲Helen的芳心呢(壞笑…)

示例2:手寫識別系統

通過kNN算法將如下圖所示的32*32數據進行判斷:
test_num

準備數據:圖像轉換爲測試向量

在這裏,需要將數據從32*32轉換爲1*1024,有兩種方法可行,第一種是書中的方法,即通過循環直接進行前後連接,第二種是直接使用numpy的flatten()方法,如下圖所示:
flatten()
這裏以書中的方法爲例:

def img2vector(filename):
    """
    change the 32*32 image matrix to 1*1024 array
    @param filename: the data set filename
    @return: the 1*1024 array
    """
    return_vect = zeros((1, 1024))
    with open(filename) as fr:
        for i in range(32):
            line_str = fr.readline()
            for j in range(32):
                return_vect[0, 32*i+j] = int(line_str[j])
    return return_vect

測試算法:使用kNN算法識別手寫數字

def handwriting_class_test():
    """
    handwriting test
    @return: screen output
    """
    hw_labels = []
    training_file_list = listdir('trainingDigits')
    m = len(training_file_list)
    training_mat = zeros((m, 1024))
    for i in range(m):
        file_name_str = training_file_list[i]
        file_str = file_name_str.split('.')[0]
        class_num_str = int(file_str.split('_')[0])
        hw_labels.append(class_num_str)
        training_mat[i, :] = img2vector('trainingDigits/%s' % file_name_str)
    test_file_list = listdir('testDigits')
    error_count = 0.0
    m_test = len(test_file_list)
    for i in range(m_test):
        file_name_str = test_file_list[i]
        file_str = file_name_str.split('.')[0]
        class_num_str = int(file_str.split('_')[0])
        vector_under_test = img2vector('trainingDigits/%s' % file_name_str)
        classifier_result = classify0(vector_under_test, training_mat, hw_labels, 3)
        print('the classifier came back with: %d, the real answer is: %d' % (classifier_result, class_num_str))
        if classifier_result != class_num_str:
            error_count += 1.0
    print('\nthe total number of errors is %d' % error_count)
    print('\nthe total error rate is %f' % (error_count/float(m_test)))

運行結果如下:
result2

更改數據量及k值可改變錯誤率。實測將k值縮小後錯誤率可降低至0.0%。

測試代碼

# coding=utf-8
"""
knn algorithm test file
"""

import kNN
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

group, labels = kNN.create_dataset()
print(kNN.classify0([0, 0], group, labels, 3))

dating_data_mat, dating_labels = kNN.file2matrix('datingTestSet2.txt')
print(dating_data_mat)
print(dating_labels)

fig = plt.figure()
ax = fig.add_subplot(111)
# ax.scatter(dating_data_mat[:, 1], dating_data_mat[:, 2], 10*np.array(dating_labels), 10*np.array(dating_labels))
type1_x = []
type1_y = []
type2_x = []
type2_y = []
type3_x = []
type3_y = []
for i in range(len(dating_labels)):
    if dating_labels[i] == 1:   # unlike
        type1_x.append(dating_data_mat[i][1])
        type1_y.append(dating_data_mat[i][2])
    if dating_labels[i] == 2:   # like
        type2_x.append(dating_data_mat[i][1])
        type2_y.append(dating_data_mat[i][2])
    if dating_labels[i] == 3:   # very like
        type3_x.append(dating_data_mat[i][1])
        type3_y.append(dating_data_mat[i][2])
type1 = ax.scatter(type1_x, type1_y, s=20)
type2 = ax.scatter(type2_x, type2_y, s=30)
type3 = ax.scatter(type3_x, type3_y, s=40)
ax.legend((type1, type2, type3), ('unlike', 'like', 'very_like'))

plt.xlabel('the Percentage of Playing Games')
plt.ylabel('the Cost of Ice-Creams per Week')
plt.title('the Data Set Distribution Figure')
plt.legend()
plt.show(fig)

norm_mat, ranges, min_val = kNN.autonorm(dating_data_mat)
print(norm_mat)
print(ranges)
print(min_val)

# kNN.dating_class_test()

# kNN.classify_person()

# kNN.handwriting_class_test()

不定期更新,未完待續。。。

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