机器学习——K近邻(KNN)算法

目录

一、knn算法概述

1.简单介绍

2.工作原理

3.knn算法中常用的距离指标

4.knn算法优势

5.knn算法一般流程

二、knn算法经典实例——海伦约会网站

三、关于天气和旅行适合度的例子

四、总结


一、knn算法概述

1.简单介绍

K近邻算法(KNN)是一种用于分类和回归的统计方法。k-近邻算法采用测量不同特征值之间的距离方法进行分类。同时要注意,KNN 算法是有监督学习中的分类算法。

2.工作原理

基于某种距离度量来找到输入样本在训练集中的“k个最近邻居”,并根据这些邻居的属性来预测输入样本的属性。如下图:

  • 绿色的点表示要预测的那个点,假设 K=3。那么 KNN 算法就会找到与它距离最近的三个点(这里用是黑色圆圈把它圈起来了),看看哪种类别多一些,比如这个例子中是红色三角形多一些,绿色点就归类为红色三角形。
  • 但是,当 K=5 的时候,判定就变成不一样了。这次变成粉色正方形多一些,所以绿点被归类成粉色正方形(这里用是灰色圆圈把它圈起来了)。

简单地说就是:“近朱者赤,近墨者黑”,而说到这个,大家是不是想到那怎样来衡量他们近不近呢,那这里,就得用距离来衡量。那说到距离,要怎么算距离?

3.knn算法中常用的距离指标

  • 欧几里得距离

看到这个名词,大家是不是很陌生,其实我听到的时候,也觉得很陌生。但是,他就是伴随我们很久的两点之间的直线距离。那这么说大家脑子中肯定浮现出这个公式了吧:

 d\left ( x,y \right )=\sqrt{\sum_{i=1}^{n}\left ( x_{i}-y_{i} \right )^{2}}

  • 曼哈顿距离

曼哈顿距离是计算两点在一个网格上的路径距离,与上述的直线距离不同,它只允许沿着网格的水平和垂直方向移动。在n维空间中,两点 之间的曼哈顿距离计算公式为:

d\left ( x,y \right )\sum_{i=1}^{n}\left | x^{i} -y^{i}\right |

4.knn算法优势

1、思想简单,理论成熟,既可以用来做分类也可以用来做回归;
2、可用于非线性分类;
3、训练时间复杂度为O(n);
4、参数量少,训练 KNN 算法时唯一需要的参数是 k 的值和我们想从评估指标中选择的距离度量的选择。

5.knn算法一般流程

1、数据准备:这包括收集、清洗和预处理数据。预处理可能包括归一化或标准化特征,以确保所有特征在计算距离时具有相等的权重。

2、选择距离度量方法:确定用于比较样本之间相似性的度量方法,常见的如欧几里得距离、曼哈顿距离等。

3、确定K值:选择一个K值,即在分类或回归时应考虑的邻居数量。这是一个超参数,可以通过交叉验证等方法来选择最优的K值。

4、找到K个最近邻居:对于每一个需要预测的未标记的样本:

5、预测

  • 对于分类任务:查看K个最近邻居中最常见的类别,作为预测结果。例如,如果K=3,并且三个最近邻居的类别是[1, 2, 1],那么预测结果就是类别1。

  • 对于回归任务:预测结果可以是K个最近邻居的平均值或加权平均值。

6、评估:使用适当的评价指标(如准确率、均方误差等)评估模型的性能。

7、优化:基于性能评估结果,可能需要返回并调整某些参数,如K值、距离度量方法等,以获得更好的性能。

二、knn算法经典实例——海伦约会网站

已知:她曾交往过三种类型的人:

  • 不喜欢的人

  • 一般喜欢的人

  • 非常喜欢的人

这些人包含以下三种特征

  • 每年获得的飞行常客里程数

  • 玩视频游戏所耗时间百分比

  • 每周消费的冰淇淋公升数

1.数据准备:从文本文件中解析数据

def file2matrix(filename):
    #打开文件
    fr = open(filename)
    #读取文件所有内容
    arrayOLines = fr.readlines()
    #得到文件行数
    numberOfLines = len(arrayOLines)
    #返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
    returnMat = np.zeros((numberOfLines,3))
    #返回的分类标签向量
    classLabelVector = []
    #行的索引值
    index = 0
    for line in arrayOLines:
        #s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ')
        line = line.strip()
        #使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
        listFromLine = line.split('\t')
        #将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
        returnMat[index,:] = listFromLine[0:3]
        #根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力
        if listFromLine[-1] == 'didntLike':
            classLabelVector.append(1)
        elif listFromLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        elif listFromLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        index += 1
    return returnMat, classLabelVector

2.分析数据:使用Matplotlib创建散点图

如(展示的代码中只画了关于游戏和冰激凌之间的关系图):

# 数据展示
def showData(datingDataMat, datingLabels):

    fig, axs = plt.subplots(nrows=2, ncols=2, sharex=False, sharey=False, figsize=(13, 8))


    LabelsColors = []
    for i in datingLabels:
        if i == 1:
            LabelsColors.append('black')
        if i == 2:
            LabelsColors.append('orange')
        if i == 3:
            LabelsColors.append('red')
 # 画出散点图,以datingDataMat矩阵的第二(玩游戏)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
 axs[1][0].scatter(x=datingDataMat[:, 1], y=datingDataMat[:, 2], color=LabelsColors, s=15, alpha=.5)
 # 设置标题,x轴label,y轴label
 axs2_title_text = axs[1][0].set_title('play_eat')
 axs2_xlabel_text = axs[1][0].set_xlabel('play_time')
 axs2_ylabel_text = axs[1][0].set_ylabel('eat_weight')
 plt.setp(axs2_title_text, size=9, weight='bold', color='red')
 plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
 plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
# 设置图例
didntLike = mlines.Line2D([], [], color='black', marker='.',
                          markersize=6, label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.',
                           markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',
                           markersize=6, label='largeDoses')
# 添加图例
axs[0][0].legend(handles=[didntLike, smallDoses, largeDoses])
axs[0][1].legend(handles=[didntLike, smallDoses, largeDoses])
axs[1][0].legend(handles=[didntLike, smallDoses, largeDoses])
# 显示图片
plt.show()

结果如图:

3.特征处理:归一化

def autoNorm(dataSet):
    #获得数据的最小值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    #最大值和最小值的范围
    ranges = maxVals - minVals
    #shape(dataSet)返回dataSet的矩阵行列数
    normDataSet = np.zeros(np.shape(dataSet))
    #返回dataSet的行数
    m = dataSet.shape[0]
    #原始值减去最小值
    normDataSet = dataSet - np.tile(minVals, (m, 1))
    #除以最大和最小值的差,得到归一化数据
    normDataSet = normDataSet / np.tile(ranges, (m, 1))
    #返回归一化数据结果,数据范围,最小值
    return normDataSet, ranges, minVals

4.训练模型 

# 分类器
# 输入:inX - 用于分类的数据(测试集);dataSet - 训练集;labes - 分类标签;K - KNN算法参数,选择距离最小的K个点
# 输出:sortedClassCount[0][0] - 分类结果
def classify0(inX, dataSet, labels, k):
    #numpy函数shape[0]返回dataSet的行数
    dataSetSize = dataSet.shape[0]
    #在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
    diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
    #二维特征相减后平方
    sqDiffMat = diffMat**2
    #sum()所有元素相加,sum(0)列相加,sum(1)行相加
    sqDistances = sqDiffMat.sum(axis=1)
    #开方,计算出距离
    distances = sqDistances**0.5
    #返回distances中元素从小到大排序后的索引值
    sortedDistIndices = distances.argsort()
    #定一个记录类别次数的字典
    classCount = {}
    for i in range(k):
        #取出前k个元素的类别
        voteIlabel = labels[sortedDistIndices[i]]
        #dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
        #计算类别次数
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
    #python3中用items()替换python2中的iteritems()
    #key=operator.itemgetter(1)根据字典的值进行排序
    #key=operator.itemgetter(0)根据字典的键进行排序
    #reverse降序排序字典
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    # import pdb
    # pdb.set_trace()
    #返回次数最多的类别,即所要分类的类别
    return sortedClassCount[0][0]


m = normDataSet.shape[0]
numTestVecs = int(m*0.1)
errorCount = 0.0
for i in range(numTestVecs):
    classifyResult = classify0(normDataSet[i,:],normDataSet[numTestVecs:m,:],
                              datingLabels[numTestVecs:m],4)
    print("分类结果:%d,真实类别:%d" % (classifyResult,datingLabels[i]))
    if(classifyResult!=datingLabels[i]):
        errorCount += 1.0
print("错误率:%f%%" %(errorCount/float(numTestVecs)*100))

可以得到如图结果:

5.最后,给定一个人的三维特征进行预测

#  进行预测


def classifyPerson():
    #输出结果
    resultList = ['讨厌','有些喜欢','非常喜欢']
    #三维特征用户输入
    ffMiles = float(input("每年获得的飞行常客里程数:"))
    precentTats = float(input("玩视频游戏所耗时间百分比:"))
    iceCream = float(input("每周消费的冰激淋公升数:"))
    #打开的文件名
    filename = "datingTestSet.txt"
    #打开并处理数据
    datingDataMat, datingLabels = file2matrix(filename)
    #训练集归一化
    normMat, ranges, minVals = autoNorm(datingDataMat)
    #生成NumPy数组,测试集
    #inArr = np.array([precentTats, ffMiles, iceCream])
    inArr = np.array([ffMiles, precentTats, iceCream])
    #测试集归一化
    norminArr = (inArr - minVals) / ranges
    #返回分类结果
    classifierResult = classify0(norminArr, normMat, datingLabels, 3)
    #打印结果
    print("你可能%s这个人" % (resultList[classifierResult-1]))

结果截图:

三、关于天气和旅行适合度的例子

1.数据准备:

  • 收集包含历史天气数据和旅行影响程度的训练集数据。
  • 将天气特征(如温度、降水量、风力等)作为输入特征,将旅行影响程度(如适合、一般、不适合等)作为目标变量。
# 准备数据:从文本文件中解析数据
def file2matrix(filename):
    #打开文件
    fr = open(filename)
    #读取文件所有内容
    arrayOLines = fr.readlines()
    #得到文件行数
    numberOfLines = len(arrayOLines)
 # 初始化特征矩阵和标签向量
    returnMat = np.zeros((numberOfLines,3))
    classLabelVector = []
    #行的索引值
    index = 1
    for line in arrayOLines:
        # 处理每行数据
        line = line.strip() # 清除空格、换行符等
        listFromLine = line.split( )# 按空格切割字符串

        #将前三列数据存入特征矩阵
        returnMat[index,:] = listFromLine[0:3]

        # 处理标签向量,1代表不合适,2代表一般,3代表合适
        if listFromLine[-1] == 'unsuited':
            classLabelVector.append(1)
        elif listFromLine[-1] == 'General':
            classLabelVector.append(2)
        elif listFromLine[-1] == 'suitable':
            classLabelVector.append(3)
        index += 1
    return returnMat, classLabelVector
  • 这里的fr.readlines() 将会返回一个包含文件中所有行的列表,每一行作为一个字符串元素存储在列表中。你可以通过遍历这个列表或者使用索引来访问每一行的内容。
  • len(arrayOLines) 中:一个内置函数 len(),用于获取列表(或其他可迭代对象)中元素的个数
  • np.zeros() 是 NumPy 库中的一个函数,用于创建一个指定形状和数据类型的数组,并用0来填充每个元素。那么上述代码就是创建一个具有 numberOfLines 行、3列的二维数组,并用0来填充每个元素。
  • classLabelVector 用于存放标签向量,returnMat 用于存放特征矩阵。
  • 对于每一行数据,先使用 line.strip() 方法清除字符串前后的空格和换行符,再使用 line.split() 方法将其按空格切割,并将结果存储在列表 listFromLine 中。
  • returnMat[index, :] = listFromLine[0:3]:从 listFromLine 中取出前三列数据,并将其存储到 NumPy 矩阵 returnMat 的第 index 行中

2.特征处理:对收集到的天气特征进行预处理,例如归一化或标准化,确保它们具有相似的数值范

围。

#归一化

def autoNorm(dataSet):
    #分别获得数据的最小。最大值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)

    #最大值和最小值的范围
    ranges = maxVals - minVals

    #初始化归一化后的数据集
    normDataSet = np.zeros(np.shape(dataSet))

    #返回dataSet的行数
    m = dataSet.shape[0]

    #原始值减去最小值
    normDataSet = dataSet - np.tile(minVals, (m, 1))

    #除以最大和最小值的差,得到归一化数据
    normDataSet = normDataSet / np.tile(ranges, (m, 1))

    #返回归一化数据结果,数据范围,最小值
    return normDataSet, ranges, minVals
  • 初始化归一化后的数据集:使用 np.zeros(np.shape(dataSet)) 创建一个与原始数据集大小相同的全零矩阵。

  • np.tile(minVals, (m, 1)) 用于生成一个形状为 (m, 1) 的矩阵,其中每一行都是最小值组成的数组。也就是将 minVals 数组在行方向上重复 m 次,在列方向上重复 1 次,形成一个与数据集相同大小的矩阵。举例理解一下:minVals = [1, 2, 3],并且 m=4,那么 np.tile(minVals, (m, 1)) 的结果为:array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]])

  • np.tile(ranges, (m, 1)) 用于生成一个形状为 (m, 1) 的矩阵,其中每一行都是范围组成的数组。与np.tile(minVals, (m, 1)) 类似,都是将一个一维数组在行方向上重复 m 次,在列方向上重复 1 次,形成一个与数据集相同大小的矩阵。

3.训练模型:使用训练集数据训练KNN模型。在这里,需要选择适当的k值和距离度量方法。

这段代码实现了 k 近邻算法的关键步骤,包括计算距离、统计出现次数并排序、返回预测结果等。它能够对于一个待分类的样本,在数据集中找到最近的 k 个样本,然后根据它们的类别进行投票,预测出该样本所属的类别。

def classify0(inX, dataSet, labels, k):
    #计算数据集的大小,即行数 
    dataSetSize = dataSet.shape[0]

    #在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
    diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
    sqDiffMat = diffMat**2

    #sum()所有元素相加,sum(0)列相加,sum(1)行相加
    sqDistances = sqDiffMat.sum(axis=1)
    #开方,计算出距离
    distances = sqDistances**0.5

    sortedDistIndices = distances.argsort()
    
    classCount = {}
    for i in range(k):
        #取出前k个元素的类别
        voteIlabel = labels[sortedDistIndices[i]]
        #dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
        #计算类别次数
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
  
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    
    #返回出现次数最多的类别,即所要分类的类别
    return sortedClassCount[0][0]
  • diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet:计算输入向量和数据集中所有样本的差值(注意,这里的 inX 是某个待分类的样本,而非数据集)
  • sqDiffMat = diffMat ** 2:差值矩阵中的每个元素进行平方
  • sqDistances = sqDiffMat.sum(axis=1):将每个样本的平方差值相加,得到一个一维数组,每个元素表示该样本与 inX 的欧氏距离的平方。
  • distances = sqDistances ** 0.5:sqDistances 中的每个元素进行开方,得到样本与 inX 的欧氏距离。
  • sortedDistIndices = distances.argsort():对欧氏距离数组 distances 进行排序并返回排序后的索引值。其中:argsort() 方法是 NumPy 数组的一个函数,它会将数组中的元素按照从小到大的顺序进行排序,并返回排序后元素的索引值。
  • classCount = {}:定义一个空字典,用于记录前 k 个距离最小的样本所属的类别及其出现次数
  • sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True):将classCount 中的键值对按照值(类别出现次数)进行排序,并将排序结果存储在列表 sortedClassCount。 
  • 其中items() 方法用于返回字典中所有键值对的元组形式;operator.itemgetter(1) 表示按照元组的第二个值进行排序。也就是说,我们希望根据字典中的值(类别出现次数)进行排序;reverse 参数用于控制排序顺序,当设置为 True 时,表示按照降序进行排序。

4.预测影响程度

  • 对于新的天气样本,在训练集中找到离该样本最近的k个邻居。
  • 根据这k个邻居的旅行影响程度进行投票或加权平均来预测新样本的旅行影响程度。
  • def classifyPerson():
        #输出结果
        resultList = ['不适合','一般','适合']
        #三维特征用户输入
        precipitation = int(input("该天降水量等级:"))
        airtemperature = float(input("该天气温:"))
        windpower=int(input("该天风力等级:"))
        #打开的文件名
        filename = "travel.txt"
        #打开并处理数据
        travelDataMat, travelLabels = file2matrix(filename)
        #训练集归一化
        normMat, ranges, minVals = autoNorm(travelDataMat)
        #生成NumPy数组,测试集
        inArr = np.array([precipitation, airtemperature,windpower])
        #测试集归一化
        norminArr = (inArr - minVals) / ranges
        #返回分类结果
        classifierResult = classify0(norminArr, normMat, travelLabels, 3)
        #打印结果
        print("这天出去玩的推荐程度是:%s" % (resultList[classifierResult-1]))
#整体思路流程
def main():
    # 打开的文件名
    filename = 'D:\machinelearning\second\travel.txt'

    # 将返回的特征矩阵和分类向量分别存储到travelDataMat和travelLabels中
    travelDataMat, travelLabels = file2matrix(filename)
    # print(travelDataMat)
    # print(travelLabels)
    # showData(travelDataMat,travelLabels)

    # 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
    normDataSet, ranges, minVals = autoNorm(travelDataMat)
    # print(normDataSet)
    # print(ranges)
    # print(minVals)

    # 获得normMat的行数, 百分之十的测试数据的个数
    m = normDataSet.shape[0]
    numTestVecs = int(m*0.1)
    #分类错误计数
    errorCount = 0.0
    for i in range(numTestVecs):
        classifyResult = classify0(normDataSet[i,:],normDataSet[numTestVecs:m,:]
                                travelLabels[numTestVecs:m],4)
        print("分类结果:%d,真实类别:%d" % (classifyResult,travelLabels[i]))
        if(classifyResult!=travelLabels[i]):
            errorCount += 1.0
    print("错误率:%f%%" %(errorCount/float(numTestVecs)*100))

if __name__ == '__main__':
    classifyPerson()

运行结果:

四、总结

错误

  • 在运行天气和旅行适合度代码时,出现以下错误

查看资料后发现是切割时发生问题,一开始用如下这句代码进行切割,但是由于我的travel.txt文件中列与列之间是用空格分开的(如图是一部分数据),所以导致报错,后将如下代码修改成line.split( )后,即可成功运行。

#修改前
 listFromLine = line.split('\t')
#修改后
 listFromLine = line.split( )

心得

  • knn算法是分类数据最简单最有效的算法, 其学习基于实例, 使用算法时我们必须有接近实际数据的训练样本数据. K-近邻算法必须保存全部数据集, 如果训练数据集的很大, 则会占用大量的存储空间. 在学习knn算法时在思路方面能理清,但是具体实现代码还是不够熟悉,会在接下来的学习中加强自己这方面知识,做到得心应手。

而我也学习并参考其他资料做了以下总结:

  1. 数据预处理:KNN算法对数据预处理非常敏感。特征之间的尺度差异或数据中的噪声可能会对算法的性能产生负面影响。因此,在应用KNN算法之前,通常需要对特征进行标准化、归一化或其他预处理步骤。

  2. k值的选择:k值是指选择最近邻居的数量。选择合适的k值对于KNN算法的性能至关重要。较小的k值可能导致过拟合,而较大的k值可能导致欠拟合。因此,需要通过交叉验证或其他技术来选择合适的k值。

  3. 距离度量方法:KNN算法中常用的距离度量方法包括欧氏距离、曼哈顿距离、余弦相似度等。选择合适的距离度量方法取决于数据的特征及其类型。在实验中,可以尝试不同的距离度量方法,并比较它们的效果。

  4. 决策规则:KNN算法的决策规则可以是多数表决或加权投票。在分类问题中,多数表决规则选择k个最近邻居中出现最频繁的类别作为预测结果。加权投票规则根据距离远近对邻居的贡献进行加权,更近的邻居具有更大的影响力。

  5. 计算效率:KNN算法需要计算待预测样本与所有训练样本之间的距离,因此计算复杂度较高。当训练集较大时,算法的计算效率可能会下降。可以使用一些优化方法,如KD树、球树等来加速计算过程。

  6. 特征选择:选择合适的特征对于KNN算法的性能也非常重要。通过分析特征的相关性,可以选择具有较高信息量的特征,并将其用于训练模型。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值