KNN算法概述
简单来说,k-近邻算法采用测量不同特征值之间的距离方法进行分类。
KNN不需要训练算法,直接将一个测试样本的每个特征与训练集中各个样本的对应的每个特征进行比较,然后算法提取出训练样本中特征最相似(即 距离最近)的样本的分类标签。
一般来说,我们只选择训练样本中前k个最相似的样本,这就是k-近邻算法中k的出处,通常k为整数且k <= 20。最后,从这k个最相似的样本中选择出现次数最多的类别标签作为该测试样本的标签。
k-近邻算法
优点:精度高、对异常值不敏感、无数据输入假定
缺点:计算复杂度高、空间复杂度高
使用数据范围:数值型和标称型
标称型:标称型目标变量的结果只在有限目标集中取值,如真与假,0、1与2等 (标称型目标变量主要用于分类)
数值型:数值型目标变量则可以从无限的数值集合中取值,如0.100,42.001等 (数值型目标变量主要用于回归分析)
pseudo-code:
对测试集中每个样本一次执行以下操作:
(1)计算一直类别数据集中的点与当前测试点之间的距离;
(2)按照距离递增次序排序;
(3)选取与当前测试点距离最小的k个点;
(4)确定前k个点所在类别的出现频率;
(5)返回前k个点出现频率最高的类别作为当前测试点的预测分类;
示例:改进约会网站的配对效果
训练样本:1000
类别:不喜欢的人0、魅力一般的人1、极具魅力的人2
三种特征(数值型):(比较奇怪)
1、 每年获得的飞行常客里程数
2、 玩游戏所耗时间百分比
3、 每周消费的冰淇淋公斤数
# 1 解析文本文件中的数据
def file2matrix(filename):
fr = open(filename)
# 按行读取所有内容
array_lines = fr.readlines()
# 训练样本数目
number_of_lines = len(array_lines)
# 定义返回的特征矩阵(数组类型)
returnmat = zeros((number_of_lines, 3)) # 3个特征
classlabelvector = []
index = 0
# 按行遍历所有内容
for line in array_lines: # " a b c 1 "
# strip() 处理的时候,如果不带参数,默认是清除两边的空白符,例如:/n, /r, /t, ' ')
line = line.strip() # "a b c 1"
# 以空格为分隔符,包含 \n
listfromline = line.split() # ['a', 'b', 'c', '1']
# 每行的前三个元素存入特征矩阵
returnmat[index, :] = listfromline[0:3] # ['a', 'b', 'c']
# numbers = [ int(x) for x in numbers ]
# 将列表的字符串逐一转为float
# map(func, *iterables) 返回迭代器,所以需要list()
returnmat[index, :] = list(map(float, returnmat[index, :]))
# 逐一存入 标签向量
classlabelvector.append(int(listfromline[-1])) # label[1]
index += 1
return returnmat, classlabelvector
#2 数据可视化,使用Matplotlib创建散点图
def fig_show():
# 创建窗口
fig = plt.figure()
# 参数349的意思是:将画布分割成3行4列,图像画在从左到右从上到下的第9块
ax = fig.add_subplot(111)
# 个性化标记散点图上的点
# 参数 x, y, size, color, maker
ax.scatter(datingdatamat[:, 0], datingdatamat[:, 1], 15.0 * array(datinglabels), 15.0 * array(datinglabels))
# 标记横纵坐标轴
plt.xlabel('mile')
plt.ylabel('game %')
plt.show()
#3 数据归一化 x-minvals/(maxval-minvals)
# 不同特征的数值之间差距较大,数值大的特征严重影响计算结果
def autonorm(dataset):
# 每列的最小值和最大值,尺寸(1,3)
minvals = dataset.min(0)
maxvals = dataset.max(0)
ranges = maxvals - minvals
# 创建尺寸大小为数据集的新矩阵
normdataset = zeros(shape(dataset))
m = dataset.shape[0]
# 当前值 - 最小值
# tile将变量内容复制成输入矩阵同样大小的矩阵
normdataset = dataset - tile(minvals, (m, 1))
# 对于某些数值处理软件包, / 可能意味着矩阵除法
# numpy中, / 表示对应位相除;linalg.solve(matA,matB)表示矩阵除法
normdataset = normdataset / tile(ranges, (m, 1))
return normdataset, ranges, minvals
"""
#4 构建分类器
Parameters:
inx - 测试集
dataset - 训练集
labels - 训练样本标签集
k - 取距离最小的前k个训练样本
Returns:
sortedclasscount[0][0] - 距离最近的k个样本中出现次数最多的标签
"""
def classify0(inx, dataset, labels, k):
datasetsize = dataset.shape[0] # dataset的大小,样本容量
diffmat = tile(inx, (datasetsize, 1)) - dataset # inx与每个样本的相减项
sqdiffmat = diffmat ** 2 # 相减项平方
sqdistances = sqdiffmat.sum(axis=1) # 计算每行的和
distances = sqdistances ** 0.5 # 开方
sorteddistindicies = distances.argsort() # 从小到大排序,输出索引
classcount = {} # 创建字典
for i in range(k): # 遍历前k行,{'labels':出现次数}
voteilabel = labels[sorteddistindicies[i]]
# 按键名找键值,如果找不到键值则在后面输出0
classcount[voteilabel] = classcount.get(voteilabel, 0) + 1
# items()将字典分解为元组列表 [(),()];sorted()根据第二个(1域)域从大到小排序
# classcount.items()为dict_items类型,sorted返回一个列表类型
sortedclasscount = sorted(classcount.items(), key=operator.itemgetter(1), reverse=True)
return sortedclasscount[0][0] # 返回出现次数最多的label
# 5 测试分类器分类效果
def dating_class_test():
ho_ratio = 0.1 # 测试样本率
dating_data_mat, dating_labels = file2matrix('datingTestSet2.txt') # 读取数据
norm_mat, ranges, minvals = autonorm(dating_data_mat) # 归一化
m = norm_mat.shape[0]
num_testvecs = int(m * ho_ratio) # 测试样本数量
error_count = 0.0 # 错误计数器
# 逐一分类,得出分类错误率
for i in range(num_testvecs):
# 测试集是所有样本的前10%
classifier_result = classify0(norm_mat[i, :], norm_mat[num_testvecs:m, :],
dating_labels[num_testvecs:m], 3)
print('the classifier came back with: %d, the real answer is: %d' % (classifier_result, dating_labels[i]))
if classifier_result != dating_labels[i]:
error_count += 1.0
print('the total error rate is: %f' % (error_count / float(num_testvecs)))
示例:手写识别系统
训练样本:2000 测试样本:900
类别:0-9
1024个特征(32x32个像素)
分类器为上一个实例的 classify0()
def img2vector(filename):
return_mat = zeros((1, 1024))
fr = open(filename)
for i in range(32):
# readlines()是按行读取所有行,readline()是只读一行
line_str = fr.readline()
for j in range(32):
# 存入return_mat的第一行
return_mat[0, 32 * i + j] = int(line_str[j])
return return_mat
#2 手写数字识别算法
def handwriting_classtest():
hw_labels = [] # 创建labels列表
training_filelist = listdir('digits/trainingDigits') # 将文件夹各个文件的文件名存入list
m_train = len(training_filelist) # 获取文件夹中的子文件(对于都为文件的文件夹)
training_mat = zeros((m_train, 1024)) # 创建新矩阵
# 遍历每个文件,得到标签和数字的向量
for i in range(m_train):
file_namestr = training_filelist[i]
filestr = file_namestr.split('.')[0]
class_numstr = int(filestr.split('_')[0])
hw_labels.append(class_numstr)
training_mat[i, :] = img2vector('digits/trainingDigits/%s' % file_namestr)
test_filelist = listdir('digits/testDigits')
error_count = 0.0
m_test = len(test_filelist)
for i in range(m_test):
file_namestr = test_filelist[i]
filestr = file_namestr.split('.')[0]
class_numstr = int(filestr.split('_')[0])
test_vector = img2vector('digits/testDigits/%s' % file_namestr)
classifier_result = classify0(test_vector, training_mat, hw_labels, 3)
print('the classifier came back with:%d, the real answer is:%d' % (classifier_result, class_numstr))
if classifier_result != class_numstr: error_count += 1.0
print('the total number of errors is:%d' % error_count)
print('the total error rate is:%f' % (error_count / float(m_test)))
KNN小结
KNN是分类数据最简单最有效的算法,使用算法时我们必须有接近实际数据的训练样本数据。
KNN必须保存全部数据集,如果训练数据集很大,必须使用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。
KNN另一个缺陷是它无法给出任何数据的基础结构信息,因此我们也无法知晓平均实例样本和典型实例样本具有什么特征。