1 k-近邻算法概述

k-近邻算法,采用测量不同特征值之间的距离方法进行分类。

工作原理:
存在一个样本数据集,也成为训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中的每一数据与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。


2 k-近邻算法伪代码

对未知类别属性的数据集中的每个点依次执行以下操作:
(1)计算已知类别数据集中的点与当前点之间的距离;
(2)按照距离递增次序排序;
(3)选取与当前点距离最小的k个点;
(4)确定前k个点所在类别的出现频率;
(5)返回前k个点出现频率最高的类别作为当前点的预测分类。

3 欧氏距离(Euclidean Distance)


欧氏距离(Euclidean Distance)


欧氏距离是最易于理解的一种距离计算方法,源自欧氏空间中两点间的距离公式。

(1)二维平面上两点a(x1,y1)与b(x2,y2)间的欧氏距离:

《机器学习实战》学习笔记(1)——k-近邻算法_数据

(2)三维空间两点a(x1,y1,z1)与b(x2,y2,z2)间的欧氏距离:

《机器学习实战》学习笔记(1)——k-近邻算法_标签_02

(3)两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的欧氏距离:

《机器学习实战》学习笔记(1)——k-近邻算法_算法_03

  也可以用表示成向量运算的形式:

《机器学习实战》学习笔记(1)——k-近邻算法_标签_04


4 k-近邻算法的优点与缺点

(1)优点

精度高、对异常值不敏感、无数据输入假定。

(2)缺点

计算复杂度高、空间复杂度高

(3)缺陷

  1. k-近邻算法是基于实例的学习,使用算法时,必须有接近实际数据的训练样本数据,必须保存全部数据集,如果训练数据集过大,必须使用大量的存储空间
  2. 由于必须对数据集中的每个数据计算距离值,实际使用时,可能非常耗时
  3. 无法给出任何数据的基础结构信息,因此我们也无法知晓平均实例样本和典型实例样本具有什么样的特征

5 Python代码实现

(1)创建数据集

def create_data_set():
    group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
    labels = ['A', 'A', 'B', 'B']
    return group, labels

(2)构造 kNN 分类器

def classify0(inX, dataSet, labels, k):
    """
    分类器 v1.0
    :param inX: 用于分类的输入向量
    :param dataSet: 输入的训练样本集
    :param labels: 标签向量(标签向量的元素数目和矩阵 dataset 的行数相同)
    :param k: 用于选择最近邻居的数目
    :return: 排序首位的 label

    对未知类别属性的数据集中的每个点依次执行以下操作:
    1、计算已知类别数据集中的点与当前点之间的距离
    2、按照距离递增次序排序
    3、选取与当前点距离最小的 k 个点
    4、确定前 k 个点所在类别的出现频率
    5、返回前 k 个点出现频率最高的类别作为当前点的预测分类
    """
    # ndarray.shape 数组维度的元组,ndarray.shape[0]表示数组行数,ndarray.shape[1]表示列数
    dataSetSize = dataSet.shape[0]
    # print(dataSetSize)

    # 将输入的 inX(1*2) 进行扩展,扩展为 4*2 矩阵,使其与训练样本集中的数据(4*2)矩阵作减法
    diffMat = tile(inX, (dataSetSize, 1)) - dataSet
    # print(diffMat)

    # 将 差值矩阵 的每一项乘方
    sqDiffMat = diffMat**2
    # print(sqDiffMat)

    # 在指定的轴向上求得数组元素的和(横向)(行)
    sqDistances = sqDiffMat.sum(axis=1)
    # print(sqDistances)

    # 开方
    distances = sqDistances**0.5
    # print(distances)

    # 将 distances 数组的元素排序 返回由其索引组成的 list
    sortedDistIndicies = distances.argsort()
    # print(sortedDistIndicies)

    # classCount 字典用于类别统计
    classCount = {}

    # 遍历 sortedDistIndicies list,依次获取最近的 k 个邻居对应的 label
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        # print(voteIlabel)

        # 若 classCount 字典中不存在 当前 voteIlabel ,则置该 key voteIlabel 对应的 value 为 0
        # 否则 +1
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
        # print(classCount)

    # print(classCount)

    # 将 classCount 字典进行排序,按照 items 的值,倒序(从大到小排列)
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    # print(sortedClassCount)

    # 将排序首位的 label 作为返回值
    return sortedClassCount[0][0]

# print(classify0([0, 0], group, labels, 3))

6 示例:约会网站相亲对象与手写数字识别系统

(1)约会网站数据

"""
在约会网站上使用 kNN
1.收集数据: 提供文本文件
2.准备数据: 使用 Python 解析文本文件
3.分析数据: 使用 Matplotlib 画二维扩散图
4.训练算法: 此步骤不适合 k-近邻算法
5.测试算法:
    测试样本与非测试样本的区别在于:
        测试样本是已经完成分类的数据,如果预测分类与实际类别不用,则标记为一个错误
6.使用算法: 产生简单的命令行程序,然后可以输入一些特征数据以判断对方是否为自己喜欢的类型
"""
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from kNN import classify0


def file2matrix(filename):
    """
    将读取的文件转换为矩阵
    :param filename: 文件名
    :return: 转换后的矩阵
    """
    # 打开文件
    fr = open(filename)

    # 将文件内容按行读取为一个 list
    arrayOLines = fr.readlines()

    # 获取 list 的长度,即文件内容的行数
    numberOfLines = len(arrayOLines)

    # 生成一个 numberOfLines*3 并以 0 ,进行填充的矩阵
    returnMat = np.zeros((numberOfLines, 3))

    # 分类标签 向量
    classLabelVector = []

    #
    index = 0

    # 遍历读入文件的每一行
    for line in arrayOLines:
        # 截取掉所有的回车符
        line = line.strip()

        # 将 line 以空格符进行分割
        listFromLine = line.split('\t')

        # index 行的所有元素替换为 listFromLine 中的 [0:3]
        returnMat[index,:] = listFromLine[0:3]

        # 分类标签向量 list 中添加 listFromLine 中的最后一项
        classLabelVector.append(int(listFromLine[-1]))

        #
        index += 1
    return returnMat, classLabelVector

datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')


def get_figure(datingDataMat, datingLabels):
    """
    直接浏览文本文件方法非常不友好,一般会采用图形化的方式直观地展示数据
    :param datingDataMat:
    :param datingLabels:
    :return:
    """
    fig = plt.figure()
    ax = fig.add_subplot(111)

    # 使用 datingDataMat 矩阵的第二、第三列数据
    # 分别表示特征值“玩视频游戏所消耗时间百分比”和“每周所消费的冰淇淋公升数”
    # ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2])

    # 利用变量 datingLabels 存储的类标签属性,在散点图上绘制色彩不等,尺寸不同的点
    # scatter plot 散点图
    ax.scatter(datingDataMat[:,0], datingDataMat[:,1],
               15.0*np.array(datingLabels), 15.0*np.array(datingLabels))
    plt.show()

# get_figure(datingDataMat, datingLabels)


def autoNorm(dataSet):
    """
    方程中数字差值最大的属性对计算结果的影响最大,在处理这种不同范围的特征值时,采用将数值归一化的方法
    :param dataSet: 输入的数据集
    :return: 归一化后的数据集
    """
    # dataSet.min(0) 中的参数 0 使得函数可以从列中选取最小值,而不是选当前行的最小值
    # minVals 储存每列中的最小值
    minVals = dataSet.min(0)

    # maxVals 储存每行中的最小值
    maxVals = dataSet.max(0)

    # 求得差值
    ranges = maxVals - minVals

    #
    # normDataSet = np.zeros(np.shape(dataSet))

    # 将数据集 dataSet 的行数放入 m
    m = dataSet.shape[0]

    # 归一化
    normDataSet = dataSet - np.tile(minVals, (m,1))
    normDataSet = normDataSet/np.tile(ranges, (m,1))
    return normDataSet, ranges, minVals

# normDataSet, ranges, minVals = autoNorm(datingDataMat)

def datingClassTest():

    # 选择 10% 的数据作为测试数据,90% 的数据为训练数据
    hoRatio = 0.10

    # 将输入的文件转换为 矩阵形式
    datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')

    # 将特征值归一化
    normDataSet, ranges, minVals = autoNorm(datingDataMat)

    # 计算测试向量的数量
    m = normDataSet.shape[0]
    numTestVecs = int(m*hoRatio)

    # 错误数量统计
    errorCount = 0.0

    # 遍历 测试向量
    for i in range(numTestVecs):

        # # 取 数据集 的后 10% 作为测试数据,错误率为 5%

        # 调用 classify0() 函数
        # 以归一化后的的数据集 normDataSet 的第 i 行数据作为测试数据,
        # 以 numTestVecs:m 行数据作为训练数据,
        # datingLabels[numTestVecs:m] 作为标签向量,
        # 选择最近的 3 个邻居
        classifierResult = classify0(normDataSet[i,:], normDataSet[numTestVecs:m,:],
                                     datingLabels[numTestVecs:m], 3)

        # 打印 预测结果 与 实际结果
        print("the classifier came back with: %d,"
              "the real answer is: %d " % (classifierResult, datingLabels[i]))

        # 当预测失败时,错误数量 errorCount += 1
        if classifierResult != datingLabels[i]:
            errorCount += 1.0

        # # -----------------------------------------------------------------------
        # # 取 数据集 的后 10% 作为测试数据,错误率为 6%
        # classifierResult = classify0(normDataSet[m-numTestVecs+i, :], normDataSet[:m-numTestVecs, :],
        #                              datingLabels[:m-numTestVecs], 3)
        #
        # print("the classifier came back with: %d,"
        #       "the real answer is: %d " % (classifierResult, datingLabels[m-numTestVecs+i]))
        #
        # if classifierResult != datingLabels[m-numTestVecs+i]:
        #     errorCount += 1.0
        # # -----------------------------------------------------------------------


    print("the total error rate is : %f" % (errorCount/float(numTestVecs)))

# datingClassTest()

def classifyPerson():
    # 预测结果 list
    resultList = ['not at all', 'in small doses', 'in large doess']

    # 获取用户输入
    percentTats = float(input('percentage of time spent playing video games?'))
    iceCream = float(input('liters of ice cream consumed per year?'))
    ffMile = float(input('frequent flier miles earned per year?'))

    # 归一化数据集
    normDataSet, ranges, minVals = autoNorm(datingDataMat)

    # 将用户输入转化为一个 Matrix
    inArr = np.array([ffMile, percentTats, iceCream])

    # 调用 classify0() ,将用户输入矩阵归一化后进行运算
    classifierResult = classify0((inArr - minVals)/ranges, normDataSet, datingLabels, 3)

    # 打印预测结果
    print('You will probably like this person: ', resultList[classifierResult])

classifyPerson()

(2)手写数字识别系统

import numpy as np
from os import listdir
from kNN import classify0

def img2vector(filename):
    returnVect = np.zeros((1,1024))
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0, 32*i + j] = int(lineStr[j])
    return returnVect

# print(img2vector('testDigits/0_13.txt')[0, 32:63])
# print(listdir('testDigits'))

def handwritingClassTest():
    # 标签向量
    hwLabels = []

    # trainingDigits 目录下的文件 list
    traingingFileList = listdir('trainingDigits')

    # trainingDigits 目录下的文件个数
    m = len(traingingFileList)

    # 1*1024 由 0 填充的矩阵
    traingingMat = np.zeros((m,1024))

    # 遍历 trainingDigits 下的所有文件
    for i in range(m):
        # 获取当前文件的文件名
        fileNameStr = traingingFileList[i]

        # 获取当前文本所代表的数值
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])

        # 在标签向量 list 中 添加此数值
        hwLabels.append(classNumStr)

        # 训练矩阵第 i 行填充当前打开文件的 1024 个字符
        traingingMat[i,:] = img2vector('trainingDigits/{}'.format(fileNameStr))

    # testDigits 目录下的文件名称 list
    testFileList = listdir('testDigits')

    # 错误率
    errorCount = 0.0

    # 测试文件的数量
    mTest = len(testFileList)

    # 遍历测试文件
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/{}'.format(fileNameStr))
        classifierResult = classify0(vectorUnderTest, traingingMat, hwLabels, 3)
        print('the classifier came back with: %d,'
              'the real answer is: %d' % (classifierResult, classNumStr))
        if classifierResult != classNumStr:
            errorCount += 1.0
    print('the total number of errors is: %d' % errorCount)
    print('the total error rate is: %f' % (errorCount/float(mTest)))

# handwritingClassTest()


# the total number of errors is: 10
# the total error rate is: 0.010571
# 错误率 1.06%

def my_handwritingClassTest():
    hwLabels = []
    traingingFileList = listdir('trainingDigits')
    m = len(traingingFileList)
    traingingMat = np.zeros((m,1024))
    for i in range(m):
        fileNameStr = traingingFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        hwLabels.append(classNumStr)
        traingingMat[i,:] = img2vector('trainingDigits/{}'.format(fileNameStr))
    testFileList = listdir('test_data')
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('test_data/{}'.format(fileNameStr))
        classifierResult = classify0(vectorUnderTest, traingingMat, hwLabels, 3)
        print('the classifier came back with: %d,'
              'the real answer is: %d' % (classifierResult, classNumStr))
        if classifierResult != classNumStr:
            errorCount += 1.0
    print('the total number of errors is: %d' % errorCount)
    print('the total error rate is: %f' % (errorCount/float(mTest)))

my_handwritingClassTest()

"""
the classifier came back with: 3,the real answer is: 3
the classifier came back with: 6,the real answer is: 6
the classifier came back with: 7,the real answer is: 7
the classifier came back with: 8,the real answer is: 8
the classifier came back with: 1,the real answer is: 9
the total number of errors is: 1
the total error rate is: 0.200000

可能是因为 9 写的太细长了,以至于长得像 1?
"""

7 使用 pandas 和 scikit-learn 实现书上的例子

(1)创建数据集

import numpy as np
import pandas as pd
from pandas import Series, DataFrame
def createDataSet():
    group = DataFrame([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]], columns=['feature_1', 'feature_2'])
    labels = DataFrame(['A','A','B','B'], columns=['labels'])
    data_set = group.join(labels)
    return data_set
dataSet = createDataSet()

feature_1

feature_2

labels

0

1.0

1.1

A

1

1.0

1.0

A

2

0.0

0.0

B

3

0.0

0.1

B

(2)应用 scikit-learn 中的 KNeighborsClassifier

from sklearn.neighbors import KNeighborsClassifier

# 定义一个knn分类器对象
knn = KNeighborsClassifier(algorithm='brute')

# 调用该对象的训练方法,主要接收两个参数:训练数据集及其样本标签
knn.fit(x_train, y_train)


In [2]:


x

from sklearn.neighbors import KNeighborsClassifier
import numpy as np
from pandas import Series, DataFrame

×



In [6]:


def createDataSet():
group = DataFrame([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1],[0.1,0]], columns=['feature_1', 'feature_2'])
labels = DataFrame(['A','A','B','B','B'], columns=['labels'])
data_set = group.join(labels)
return data_set
dataSet = createDataSet()
x_train = dataSet.iloc[:, :2].values
y_train = dataSet.iloc[:, -1].values



×


In [7]:


x

# 定义一个knn分类器对象
knn = KNeighborsClassifier(algorithm='brute')


×



In [8]:


x

# 调用该对象的训练方法,主要接收两个参数:训练数据集及其样本标签
knn.fit(x_train, y_train)


×


Out[8]:



KNeighborsClassifier(algorithm='brute', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=5, p=2,
           weights='uniform')





In [9]:

x



x_test = np.array([0, 0.3])


×




In [10]:

y_predict = knn.predict(x_test.reshape(1,-1))
y_predict


×


Out[10]:



array(['B'], dtype=object)





In [13]:


probility = knn.predict_proba(x_test.reshape(1,-1))
probility





×


Out[13]:


array([[ 0.4,  0.6]])




In [15]:


probility.argmax()

×

Out[15]:



1




In [18]:


# 距离升序排列
knn.kneighbors(x_test.reshape(1,-1),5,False)



×


Out[18]:


array([[3, 2, 4, 1, 0]], dtype=int64)



(3)约会网站配对数据集测试


In [3]:

import numpy as np
import pandas as pd
from pandas import Series, DataFrame


×



In [9]:

group = DataFrame([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]], columns=['feature_1', 'feature_2'])
group


×


Out[9]:



feature_1

feature_2

0

1.0

1.1

1

1.0

1.0

2

0.0

0.0

3

0.0

0.1





In [11]:


labels = DataFrame(['A','A','B','B'], columns=['labels'])
labels





×


Out[11]:



labels

0

A

1

A

2

B

3

B





In [17]:


# 效果相同
# data_set = pd.merge(group, labels, how='outer', left_index=True, right_index=True)
data_set = group.join(labels)
data_set

×


Out[17]:



feature_1

feature_2

labels

0

1.0

1.1

A

1

1.0

1.0

A

2

0.0

0.0

B

3

0.0

0.1

B





In [18]:


data_set['feature_1']





×



Out[18]:



0    1.0
1    1.0
2    0.0
3    0.0
Name: feature_1, dtype: float64



In [19]:


data_set.ix[0]


×


Out[19]:



feature_1      1
feature_2    1.1
labels         A
Name: 0, dtype: object





In [20]:


data_set['feature_1'][0]



×

Out[20]:



1.0





In [22]:



data_set.ix[0]['feature_1']


×


Out[22]:



1.0





In [24]:


data_set.iloc[0]


×


Out[24]:



feature_1      1
feature_2    1.1
labels         A
Name: 0, dtype: object





In [25]:


data_set.iloc[0, :]


×



Out[25]:



feature_1      1
feature_2    1.1
labels         A
Name: 0, dtype: object





In [26]:


data_set.iloc[:, 0]





×


Out[26]:


0    1.0
1    1.0
2    0.0
3    0.0
Name: feature_1, dtype: float64



In [27]:


data_set.iloc[:, 0].values





×


Out[27]:



array([ 1.,  1.,  0.,  0.])





In [29]:


data_set.shape



×


Out[29]:



(4, 3)




In [30]:


len(data_set.columns)


×





Out[30]:

3




In [31]:


data_set.values





×



Out[31]:


array([[1.0, 1.1, 'A'],
       [1.0, 1.0, 'A'],
       [0.0, 0.0, 'B'],
       [0.0, 0.1, 'B']], dtype=object)





In [33]:

data_set.iloc[:, :2].values





×


Out[33]:



array([[ 1. ,  1.1],
       [ 1. ,  1. ],
       [ 0. ,  0. ],
       [ 0. ,  0.1]])





In [28]:


x


def createDataSet():
group = DataFrame([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]], columns=['feature_1', 'feature_2'])
labels = DataFrame(['A','A','B','B'], columns=['labels'])
data_set = group.join(labels)
return data_set
createDataSet()


×

Out[28]:



feature_1

feature_2

labels

0

1.0

1.1

A

1

1.0

1.0

A

2

0.0

0.0

B

3

0.0

0.1

B