前言

在机器学习的世界里,分类问题是一大核心课题——从用户画像标签到推荐系统偏好推测,分类模型的身影无处不在。本篇文章主要介绍经典算法:K近邻。

本文将介绍KNN算法,接着以一个约会数据分类任务为例,去探索KNN算法的实现和评估。我们系那个会手动构建KNN模型的核心逻辑,还会利用ROC曲线与AUC值科学地评估多分类模型的性能,从而直观地判断模型的分类能力

本文会有具体和注解,让我们用最直观的方式,一起来感受KNN算法的魅力吧!

一、介绍KNN算法以及其代码实现

1.KNN是什么?

KNN(K-Nearest Neighbors,K近邻)算法是机器学习中一种简单且经典的有监督分类算法。

1.1 KNN算法的工作原理

存在一个训练样本集,样本集中每个数据都存在标签,输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据的分类标签(一般会选择前k个最相似数据)。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。

1.2 算法基本流程

(1)计算当前点与所有点之间的距离
(2)按升序排列距离
(3)选取距离最近的k个点
(4)统计这k个点所在类别出现的频率
(5)这k个点中出现频率最高的类别作为预测的分类

1.3 距离度量

算法中计算点与点之间的距离通常使用选用曼哈顿距离或者欧氏距离
本文我们选用欧式距离:
d = ( x 2 − x 1 ) 2 + ( y 2 − y 1 ) 2 d=\sqrt{(x_2-x_1)^2+(y_2-y_1)^2} d=(x2x1)2+(y2y1)2

1.4 参数k值分析

(1)k值小,相当于用较小领域中的训练实例进行预测,泛化性不佳,易出现过拟合的情况
(2)k值大,相当于用较大领域中的训练实例进行预测,易出现欠拟合的情况

1.5 KNN算法的优缺点

优点: (1)可以处理分类问题,算法原理直观,没有复杂的模型和参数需要调整
(2)对数据的分布没有特定的要求
(3)KNN还可以处理回归问题
缺点: (1)效率低,每次分类都要遍历所有训练样本,若训练数据集较大,则计算量巨大
(2)对k值敏感,过拟合、欠拟合问题难以权衡
(3)存在维数灾难问题,对样本特征的缩放敏感

2.KNN算法的代码实现

2.1 数据集准备

数据集使用的是约会数据集,大致格式如下所示:​​
​​约会数据集部分示例
数据集共有1000行,类别数为3,分别为“smallDoses”、“didntLike”、“largeDoses”。该数据集包含3个特征。

对数据进行读取与划分,简单按比例将数据划分为训练集(80%)和测试集(20%),用于后续模型训练和评估。

# 定义列名(特征1、特征2、特征3、类别)
columns = ['feature1', 'feature2', 'feature3', 'category']
df = pd.read_csv(
    'D:\\datingTestSet.txt',#读取数据集
    sep='\t',          # 数据集用制表符分隔
    header=None,       # 无表头
    names=columns,     # 手动指定列名
)

# 划分特征(X)和标签(Y)
X = df[['feature1', 'feature2', 'feature3']].values  # 特征矩阵
Y = df['category'].values  # 类别标签

# 划分训练集(前800行)和测试集(后200行)
X_train = X[:800]    # 训练特征
y_train = Y[:800]    # 训练标签
X_test = X[800:]     # 测试特征
y_test = Y[800:]     # 测试标签(转换为numpy数组)

2.2 数据预处理:特征归一化

KNN算法依赖距离计算,不同特征的数值范围差异会影响距离结果。例如,约会数据集中,特征一和特征二、三的值大小差异很大,这会影响距离结果。因此,先将特征归一化到相同范围(通常是[0,1])

def scaler(X):
    min_val = X.min(axis=0)  # 按列计算每个特征的最小值
    max_val = X.max(axis=0)  # 按列计算每个特征的最大值
    ranges = max_val - min_val  # 特征值的范围(最大值-最小值)
    X_scaled = (X - min_val) / ranges  # 归一化公式:(原始值-最小值)/范围
    return X_scaled, min_val, ranges  # 返回归一化后的数据、最小值和范围

2.3 KNN算法核心类实现

通过面向对象的方法封装KNN算法,包含初始化、训练、距离计算

class KNN():
    def __init__(self, K=5):
        # 初始化K值(默认5个近邻)
        self.K = K
        # 初始化训练数据相关变量
        self.X_train_scaled = None  # 归一化后的训练特征
        self.y_train = None  # 训练标签
        self.train_min = None  # 训练集特征最小值(用于测试集归一化)
        self.train_ranges = None  # 训练集特征范围(用于测试集归一化)
        #训练方法
    def fit(self, X_train, y_train):
        # 调用归一化函数,得到处理后的训练特征、最小值和范围
        self.X_train_scaled, self.train_min, self.train_ranges = scaler(X_train)
        self.y_train = y_train  # 保存训练标签
			 #距离计算:利用欧氏距离,对测试样本以及训练样本求距离
    def distance(self, x_test, x_train):
        return np.sqrt(np.sum((x_test - x_train) **2))
        #单个样本预测
    def predict(self, x_test):
        # 用训练集的归一化参数处理测试样本
        x_test_scaled = (x_test - self.train_min) / self.train_ranges
        
        # 计算测试样本与所有训练样本的距离,存储为(距离, 类别)元组
        dis_list = []
        for i in range(len(self.X_train_scaled)):
            dist = self.distance(x_test_scaled, self.X_train_scaled[i])
            dis_list.append((dist, self.y_train[i]))
        
        # 按距离升序排序,取前K个最近邻
        dis_list_sorted = sorted(dis_list, key=lambda x: x[0])
        K_nearlist = dis_list_sorted[:self.K]
      
        # 统计K个近邻中每个类别的出现次数(投票)
        count = {}
        for dist, type in K_nearlist:
            if type not in count:
                count[type] = 1
            else:
                count[type] += 1
        
        # 取出现次数最多的类别作为预测结果
        result_type = sorted(count.items(), key=lambda x: x[1], reverse=True)[0][0]
        return result_type

			 # 对多个测试样本批量调用predict方法
	def predicts(self, X_test):
        y_pred = []
        for x_test in X_test:
            result_type = self.predict(x_test)
            y_pred.append(result_type)
        return np.array(y_pred)  

2.4 模型训练与预测

初始化KNN模型,用训练集拟合,再对测试集进行预测,并输出部分结果进行对比

# 初始化KNN模型(K=5)
K = 5
knn = KNN(K)

# 训练模型(实际是保存并归一化训练数据)
knn.fit(X_train, y_train)

# 对测试集进行预测
y_pred = knn.predicts(X_test)

# 展示200条预测结果与真实结果的对比
for i in range(200):
    print(f"第{i+1:2d}条:分类结果:{y_pred[i]} 真实结果:{y_test[i]}")码片

部分输出结果如下图所示:
KNN算法输出结果(部分示例)

二、绘制ROC曲线

1.什么是ROC曲线

ROC曲线是一种直观评估二分类模型性能的可视化工具,通过横轴(FPR)和纵轴(TPR)的变化关系,去展示模型在不同阈值下的分类能力,是机器学习中评估分类模型的核心工具之一。

1.1 TPR与FPR

TPR:真正例率,即被模型正确预测为"正例"的样本数,占所有实际正例样本数的比例
T P R = T P T P + F N TPR=\frac{TP}{TP+FN} TPR=TP+FNTP
其中, TP:实际为正例,模型预测为正例
FN:实际为正例,模型预测为负例
FPR:假正例率,即被模型错误预测为"正例"的样本数,占所有实际负例样本数的比例
F P R = F P F P + T N FPR=\frac{FP}{FP+TN} FPR=FP+TNFP
其中, FP:实际为负例,模型预测为正例
TN:实际为负例,模型预测为负例

1.2 AUC值(曲线下面积)

(1)AUC是ROC曲线与横轴之间的面积
(2)AUC越大,模型区分正例和负例的能力越强

2.绘制ROC曲线

2.1 代码实现(只展示绘制ROC曲线的核心代码)

省去前面处理数据集合KNN算法实现的代码

# class_:类别列表
def make_roc(y_true, y_proba, class_, save_path='roc_curve.png'):
    plt.figure(figsize=(10, 8))#设置窗口
    # 计算每个类别的ROC曲线和AUC
    for i, cls in enumerate(class_):
        # 步骤1:将当前类别视为"正例"(1),其他类别视为"负例"(0)
        #因为原先有三个类别,转化后,可以将其中一个类别作为关注的正例,而其他的都是负例
        y_true_b= [1 if label==i else 0 for label in y_true]# 1表示当前类别,0表示其他
        y_score = y_proba[:, i]  # 当前类别的预测概率

        # 计算所有可能阈值下的TPR和FPR
        # 阈值即临界值,比如阈值设为0.5时,概率>=0.5判为喜欢,否则判为不喜欢
        #unique提取出其中不重复的值,将这些值作为阈值
        thresholds = np.unique(y_score)
        TPR_list = []  # 真正例率(正确预测的正例/实际正例总数)
        FPR_list = []  # 假正例率(错误预测的正例/实际负例总数)

        # 统计实际正例和负例数量
        # 因为1是正例,0是负例,对其求和,结果就是正例的数量
        positive = np.sum(y_true_b)
        negative = len(y_true_b) - positive

        # 遍历每个阈值计算指标
        for threshold in thresholds:
            #根据阈值和score的大小进行划分
            y_pred = [1 if score>=threshold else 0 for score in y_score]

            # 计算TP(真正例)和FP(假正例)
            TP = sum(1 for p, t in zip(y_pred, y_true_b) if p == 1 and t == 1)
            FP = sum(1 for p, t in zip(y_pred, y_true_b) if p == 1 and t == 0)

            # 计算TPR和FPR(避免除零错误)
            TPR = TP / positive if positive > 0 else 0
            FPR = FP / negative if negative > 0 else 0
            TPR_list.append(TPR)
            FPR_list.append(FPR)

        # 计算AUC(曲线下面积,用梯形法)
        # 按FPR排序确保曲线单调
        pairs=list(zip(FPR_list,TPR_list))
        pairs_sorted=sorted(pairs,key=lambda x:x[0]) #按照元组的第一个元素排序,即FPR的值
        fpr_sort=[p[0] for p in pairs_sorted]
        tpr_sort=[p[1] for p in pairs_sorted]

        # 计算面积
        #梯形面积:(x2-x1)*(y1+y2)/2
        auc = 0.0
        for j in range(1, len(fpr_sort)):
            auc += (fpr_sort[j] - fpr_sort[j - 1]) * (tpr_sort[j] + tpr_sort[j - 1]) / 2

        # 绘制ROC曲线
        #fpr_sort是x轴数据,tpr_sort是y轴数据
        plt.plot(fpr_sort, tpr_sort, label=f'{cls} (AUC={auc:.3f})')

        # 绘制随机猜测的基准线(AUC=0.5)
    plt.plot([0, 1], [0, 1], 'k--', label='随机猜测')

    # 图形设置
    plt.xlabel('FPR')
    plt.ylabel('TPR')
    plt.title('KNN ROC Curve')
    plt.legend() #设置图例
    plt.grid(alpha=0.3) #添加网格线,设置透明度
    # 在函数 make_roc 的 plt.show() 前添加
    print("绘制的曲线数量:", len(plt.gca().get_lines()))  # 应该等于类别数(3条曲线+1条基准线=4)
    plt.show()

结果如图所示:
ROC曲线示意图

2.2 ROC曲线分析

ROC曲线越靠近左上角,模型的分类性能越好。可见,“smallDoses”类别的曲线最靠近左上角,说明在该类别上,模型在控制假正例率的同时,又可以获得很高的真正例率,即模型对于这一类型的识别能力最强。而对于“largeDoses”的识别能力相对稍弱。

总结

本次实验围绕约会数据集,对 KNN 算法的分类能力展开实践与验证。以面向对象方式手动实现 KNN 算法,借助 ROC 曲线与 AUC 值评估多分类性能。
综合来看,KNN 算法原理简单且在本实验中表现有效,特征归一化是其关键预处理步骤。不过,KNN 作为 “惰性学习” 算法,预测时计算效率较低,后续可通过 KD 树等数据结构优化距离计算;同时,K 值选择对结果影响较大,可通过交叉验证选择最优 K 值,此外,针对largeDoses类别区分能力较弱的情况,还可尝试增加特征维度或结合其他距离度量进一步优化模型。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐