机器学习作业二之KNN算法

KNN(K- Nearest Neighbor)法即K最邻近法,最初由 Cover和Hart于1968年提出,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路非常简单直观:如果一个样本在特征空间中的K个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别 。

该方法的不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最邻近点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本。另外还有一种 Reverse KNN法,它能降低KNN算法的计算复杂度,提高分类的效率 。

KNN算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分 。

——百度百科

目录

一、算法思想:

第三次作业更新:

二、代码

函数解释:

guiyihua:

load:

euclideanDistance:

shoright 与showtest:

第三次作业更新解释:

show4:

showroc:

jg:

三、实际问题

问题一、

问题二、

四、实验结论

2024/04/06第三次实验作业:补充评估指标、ROC曲线:

使用的公式:

结果:

数据:

小总结


一、算法思想:

(算法名字中含有Nearest Neighbor,最近的邻居,可以想想一下一个人刚到一片陌生的地方,想要熟悉这个地方的一种方法就是找几个最近的邻居来了解这块地方。)

个人概括的正式一点的解释:

已经有的样本,均有n个特征值,可以用n个坐标轴表示出这个样本点的位置。

而测试集中的元素,也均有n个特征值,也可以用n个坐标轴表示出这个样本点的位置。

对于每个测试集中的元素,找到距离其最近的k个点(距离可使用欧氏距离d(x, y) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2}),在这k个点中选出数量最多的一个种类,将这个种类作为其结果。

需要注意的是,由于每个坐标的相对大小不同, 需要将数值做归一化处理。

第三次作业更新:

加入概率得分,每次得到一个结果的时候,也可以得到一个概率得分,对于每个概率得分,计算公式为P_i = \frac{​{\text{​{vote}}[\text{​{sortvote}}[0][0]]}}{​{k}},其中p为概率得分,vote[x]表示种类x的投票数量,sortvote[0][0]表示最多投票数量的种类,k为投票总数。


二、代码

思想不难,代码:

(代码下方有各个函数的解释)(2024/04/06第三次实验更新代码)

import csv
import math
import operator
from matplotlib import pyplot as plt
import numpy as np
from sklearn.metrics import auc, roc_curve

def guiyihua(train, input):
    maxval1 = 0
    minval1 = 101
    maxval2 = 0
    minval2 = 101
    for i in range( len(train) ):
        train[i][0] = float(train[i][0])
        train[i][1] = float(train[i][1])
        maxval1 = max(maxval1, train[i][0])
        minval1 = min(minval1, train[i][0])
        maxval2 = max(maxval2, train[i][1])
        minval2 = min(minval2, train[i][1])
    for i in range( len(input) ):
        input[i][0] = float(input[i][0])
        input[i][1] = float(input[i][1])
        maxval1 = max(maxval1, input[i][0])
        minval1 = min(minval1, input[i][0])
        maxval2 = max(maxval2, input[i][1])
        minval2 = min(minval2, input[i][1])
    for i in range( len(train)):
        train[i][0] = (train[i][0]-minval1)/(maxval1-minval1)
        train[i][1] = (train[i][1]-minval2)/(maxval2-minval2)
    for i in range( len(input) ):
        input[i][0] = (input[i][0]-minval1)/(maxval1-minval1)
        input[i][1] = (input[i][1]-minval2)/(maxval2-minval2)

def load(fname):
    with open(fname, 'rt') as csvfile:
        lists = csv.reader(csvfile)
        data = list(lists)
        return data
    
def euclideanDistance(atrain, ainput, needcal):
    re2 = 0
    for i in range(needcal):
        re2 += (atrain[i] - ainput[i])**2
    return math.sqrt(re2)

def jg(train, ainput, k):
    alldis = []
    needcal = len(ainput)-1 #需要计算的维度
    for i in range(len(train)):
        nowdis = euclideanDistance(train[i], ainput, needcal)
        alldis.append((train[i], nowdis))
    alldis.sort(key=operator.itemgetter(1))
    
    vote = {}
    vote["第一种"] = 0
    vote["第二种"] = 0
    for i in range(k):
        type = alldis[i][0][-1]
        if type in vote:
            vote[type] += 1
        else:
            vote[type] = 1
        
    sortvote = sorted(vote.items(), key=operator.itemgetter(1), reverse=True)#items()将字典转为列表,这样可以对第二个值进行排序
    probability = float(vote[sortvote[0][0]])/float(k)
    return sortvote[0][0], probability
 
def showright(train, input):
    plt.subplot(4, 5, 1)
    plt.title("right")
    for i in range(len(train)):
        if train[i][-1] == "第一种" :  
            plt.scatter(train[i][0], train[i][1], c = '#0066FF', s = 10, label = "第一种")
        else :
            plt.scatter(train[i][0], train[i][1], c = '#CC0000', s = 10, label = "第二种")
    for i in range(len(input)):
        if input[i][-1] == "第一种" :
            plt.scatter(input[i][0], input[i][1], c = '#00CC00', s = 60, label = "cs第一种")
            plt.scatter(input[i][0], input[i][1], c = '#0066FF', s = 30, label = "cs第一种")
            #plt.scatter(input[i][0], input[i][1], c = '#00FF33', s = 30, label = "cs第一种")
        else :
            plt.scatter(input[i][0], input[i][1], c = '#00CC00', s = 60, label = "cs第二种")
            plt.scatter(input[i][0], input[i][1], c = '#CC0000', s = 30, label = "cs第二种")
            #plt.scatter(input[i][0], input[i][1], c = '#00FFFF', s = 30, label = "cs第二种")

def showtest(train, input, re, ki, cnt):
    plt.subplot(4, 5, (int(ki/2)+1)+1)
    plt.title("k = "+ repr(ki)+" acc: "+repr(1.0*cnt/(1.0*len(input))*100 )+ '%')
    for i in range(len(train)):
        if train[i][-1] == "第一种" :
            plt.scatter(train[i][0], train[i][1], c = '#0066FF', s = 10, label = "第一种")
        else :
            plt.scatter(train[i][0], train[i][1], c = '#CC0000', s = 10, label = "第二种")
    for i in range(len(input)):
        if re[i] == "第一种" :
            plt.scatter(input[i][0], input[i][1], c = '#00CC00', s = 60, label = "cs第一种")
            plt.scatter(input[i][0], input[i][1], c = '#0066FF', s = 30, label = "cs第一种")
            #plt.scatter(input[i][0], input[i][1], c = '#00FF33', s = 30, label = "cs第一种")
        else :
            plt.scatter(input[i][0], input[i][1], c = '#00CC00', s = 60, label = "cs第二种")
            plt.scatter(input[i][0], input[i][1], c = '#CC0000', s = 30, label = "cs第二种")
            #plt.scatter(input[i][0], input[i][1], c = '#00FFFF', s = 30, label = "cs第二种")
    # 示例数据

    # 计算 ROC 曲线的参数
def showroc(actual, ki, probas):
    ract = []
    for i in range(len(actual)):
        if actual[i] == '第一种':
            ract.append(1)
        else:
            ract.append(0)
    fpr, tpr, thresholds = roc_curve(ract, probas)
    roc_auc = auc(fpr, tpr)
    # 绘制 ROC 曲线
    plt.subplot(4, 5, 10+(int(ki/2)+1)+1)
    print("fpr, tpr: ")
    for i in range(len(fpr)):
        print(repr(fpr[i])+" " + repr(tpr[i]))
    print("\n")
    plt.plot(fpr, tpr, color='darkorange', lw=2)
    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.0])
    plt.xlabel('FPR')
    plt.ylabel('TPR')
    plt.title('ROC:area' + repr(roc_auc))

def show4(re, actual, ki):
    acc, pre, rec, f1 = evaluate_results(re, actual)
    print("K值为{}时的评估指标:".format(ki))
    print("准确率: {:.2f}%".format(acc * 100))
    print("精确率: {:.2f}".format(pre))
    print("召回率: {:.2f}".format(rec))
    print("F1 Score: {:.2f}".format(f1))


def evaluate_results(predictions, actual):
    tp = 0
    fp = 0
    tn = 0
    fn = 0
    for i in range(len(predictions)):
        if predictions[i] == actual[i]:
            if predictions[i] == "第一种":
                tp += 1
            else:
                tn += 1
        else:
            if predictions[i] == "第一种":
                fp += 1
            else:
                fn += 1
                
    acc = (tp + tn) / len(predictions) 
    pre = tp / (tp + fp)
    rec = tp / (tp + fn)
    f1 = 2 * (pre * rec) / (pre + rec)
    
    return acc, pre, rec, f1

def main():
    train = load("C:\\Users\\T.HLQ12\\Desktop\\wdnmd\\python\\jiqixuexi\\train.csv")
    input = load("C:\\Users\\T.HLQ12\\Desktop\\wdnmd\\python\\jiqixuexi\\test.csv")
    guiyihua(train, input)
    # print(train)
    # print(input)
    plt.subplots_adjust(hspace=0.5, wspace=0.5)
    showright(train, input)
    #probas = np.arange(0, 1.1, 0.1)
    for ki in range (1, 19, 2):
        re = []
        probas = []
        cnt = 0
        actual = [row[-1] for row in input]
        for i in range(len(input)):
            type, pro = jg(train, input[i], ki)
            re.append(type)
            probas.append(pro)
            if type == input[i][-1]:
                cnt += 1
            print("预测:" + type + ", 实际上: " + actual[i])
        show4(re, actual, ki)
        showtest(train, input, re, ki, cnt)
        showroc(actual, ki, probas)
        print("\n")
        
    plt.show()
main()
    

函数解释:

guiyihua:

不会归一化的英文,从训练集和测试集中找出一个最大值和最小值。然后把训练集和测试集的数据都减去最小值,再除以最大值减最小值即可。

load:

使用with open可以不用人为关闭文件。其中csv.reader会返回一个迭代器,配合list将data赋值为二维数组。

euclideanDistance:

欧式距离,就是把所有维度平方下相加,然后再返回开根号的值。

shoright 与showtest:

这两个函数是用于绘制散点图的。Subplot中第一个参数是行数,第二个参数是列数。第三个参数是第几个部分。Title中可以设置这张图的标题。训练集中每个种类的颜色都不一样,点是使用scatter打上去的,其中第一个参数是这个点在第一条坐标轴上对应的值。第二个点是第二个坐标轴上对应的值,c是颜色。S是点的大小。 label是这个点的标签。循环遍历训练集和测试的每个点,就可以绘制出一张散点图。先绘制一个大绿点,再绘制一个中点,即可展现出一个以绿色为底的测试点。

第三次作业更新解释:
show4:

展现精确率,准确率,召回率,f1分数4个指标,计算公式底下有。

showroc:

展现roc曲线,设第一种为正例,ract是对于actual数组中第一种为1,其他为0的映射。

fpr和tpr是假正例率和真正例率,可以把真实分类ract和jg中得到的probas概率得分传进去,会返回fpr、tpr和阈值。阈值是roc_curve方法自己确定的。

roc_auc是roc曲线的面积,可以直接用auc函数得到。

最后就是用plot展示二维坐标,roc曲线的横坐标是fpr,纵坐标是tpr。

jg:

这个是judge的缩写,判断输入的测试集中的一个元素的种类。函数的参数有训练集,一个输入的值和一个k。遍历训练集中的所有元素,算出距离测试点的欧式距离,然后添加到alldis数组里。最后对数组进行排序(参数中意味按照元组中第一个值排序,默认从大到小)。然后创建一个vote字典。这个字点的第一个值是种类,第二个值是种类的个数。循环遍历距离数组,每次碰到一个种类,就把这个种类的数量加一。循环结束后,对这个字典的第二个值进行排序(排序中参数:第一个item:将字典vote转换为一个包含键值对的列表, 第二个:对下标1进行排序,第三个:从大到小排序),选出最大数量的种类作为这个测试元素的结果。

probas是概率得分,在knn算法中,每个概率得分就用返回的结果那一类的投票数除以k即可。

三、实际问题

问题一、

如图所示,这个报错是因为vote中不存在vote括号中的值,修改为:

即可。

问题二、

这个问题不知道发生的原因是什么,查询资料本来以为是scatter可以使用切分,但是实际上没有办法使用,最后就替换成了循环遍历每个点来绘制散点图的方式。

四、实验结论

2024/04/06第三次实验作业:补充评估指标、ROC曲线:

使用的公式:

混淆矩阵:

        \begin{bmatrix} \text{TP} & \text{FP} \\ \text{FN} & \text{TN} \end{bmatrix}

准确率:

Accuracy = \frac{TP + TN}{TP + FP + FN + TN}

精确率:

Precision = \frac{TP}{TP + FP}

召回率:

Recall = \frac{TP}{TP + FN}

F1分数:

F1 = 2 \times \frac{Precision \times Recall}{Precision + Recall}

真正例率:

TPR = \frac{TP}{TP + FN}

假正例率:

FPR = \frac{FP}{FP + TN}

结果:

(图中绿色为底的是测试集,小点为训练集)

ROC曲线形成的面积为AUC,AUC越大,模型的效果越好,当AUC大于0.5,也就是在y=x上方的时候,这个模型才比较正确,当在下方的时候,说明这个模型比随机预测的还糟糕,可以通过反向预测来提高模型的正确性。

从结果分析上来看,当k = 9时,AUC的面积最大,当K=1时,准确率最大。

数据:

测试集:

训练集:

小总结

KNN的优势:

  1. 简单易理解: KNN 算法非常直观和易于理解。它基于实例之间的距离来进行分类或回归预测,因此概念上很容易理解。

  2. 无需训练阶段: KNN 是一种基于实例学习的算法,它不需要显式的训练阶段,只需要存储训练数据。这使得在新数据到来时可以直接使用已有的训练数据进行预测。

  3. 适用于多类别问题: KNN 可以很容易地应用于多类别分类问题,而不需要额外的修改或调整。

  4. 适应性强: KNN 不对数据做出明确的假设,因此对于非线性的数据或复杂的决策边界也能较好地适应。

劣势:

  1. 计算复杂度高: KNN 需要在预测时计算新样本与所有训练样本之间的距离,因此在数据量大时计算复杂度较高,预测速度相对较慢。

  2. 存储开销大: KNN 需要存储全部的训练数据,因此对于大规模数据集会占用较大的内存空间。

  3. 灵敏度高: KNN 对于异常值(噪声)和不平衡数据集比较敏感,可能会导致预测不稳定或偏差较大。

  4. 需要选择合适的 K 值: KNN 中的 K 值代表着选择邻居的数量,需要手动选择一个合适的 K 值,不同的 K 值可能导致不同的预测结果。

  • 24
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值