簡述
kNN算法(中文翻譯:k-近鄰算法)是機器學習分類算法的基礎部分,也是比較簡單的算法之一。它的內容和原理並不複雜,但是計算量比較大,即時間複雜度和空間複雜度都比較高。書中以約會網站和手寫數字識別系統爲例。在這裏,筆者也將從這兩個例子下手,但是對部分代碼進行了改進,以便適應Python3的編程環境。
算法描述
kNN的k指的是在新數據與樣本數據進行比對時,只選取前k個最相近的數據。
kNN算法就是對未知類別屬性的數據集中的每個點依次執行以下操作:
- 計算已知類別數據集中的點與當前點之間的距離(歐氏距離:
d=(xA−xB)2+(yA−yB)2−−−−−−−−−−−−−−−−−−−√
); - 按照距離遞增次序排序;
- 選取與當前點距離最小的k個點;
- 確定前k個點所在類別的出現頻率;
- 返回前k個點出現頻率最高的類別作爲當前點的預測分類。
特點
- 優點:精度高,對異常值不敏感,無數據輸入假定。
- 缺點:計算複雜度較高,空間複雜度較高。
- 適用範圍:數值型和標稱型。數據需帶有目標數據,即人工標籤。標籤形式可以是文件名,也可以是文檔內的某一列。
算法處理一般流程
- 收集數據
- 準備數據:結構化的數據格式,有自己的數據格式即可。
- 分析數據
- 訓練算法:此步驟不適用於kNN,但是爲了明確一般流程,仍然加上。
- 測試算法:計算錯誤率。
- 使用算法:首先輸入樣本數據和結構化的輸出結果,然後運行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. 話說貌似打會兒遊戲還是很受歡迎的哈~
可將其繪製爲“冰激凌-遊戲時間圖”如下:
準備數據:歸一化
歸一化就是把數據範圍限制在某個明確的範圍之內,比如接下來我們就需要把數據統一到(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))
代碼結果演示如下:
使用算法
將此算法應用於具體的應用之內,根據一個人的三個標籤特徵判斷他對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])
算法運行結果如下:
怎麼樣,你是否也能捕獲Helen的芳心呢(壞笑…)
示例2:手寫識別系統
通過kNN算法將如下圖所示的32*32數據進行判斷:
準備數據:圖像轉換爲測試向量
在這裏,需要將數據從32*32轉換爲1*1024,有兩種方法可行,第一種是書中的方法,即通過循環直接進行前後連接,第二種是直接使用numpy的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)))
運行結果如下:
更改數據量及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()
不定期更新,未完待續。。。