写在前面今天咱们开始学第一个真正意义上的分类算法——KNNK近邻算法。KNN大概是所有机器学习算法里最直观、最好理解的一个不需要复杂的数学推导核心思想就是“物以类聚人以群分”。全程不用课件里的电影分类案例我换一个更贴近生活的场景——“相亲匹配预测”帮你看懂KNN到底在干什么。第一部分KNN算法的核心思想1.1 先讲个故事相亲角的大爷大妈你去人民公园的相亲角想帮朋友找个对象。朋友的条件是28岁、身高178cm、程序员、年收入35万。你该怎么判断“这个人大概是什么类型”聪明的做法是找已经配好对的样本——看看这个圈子里的历史数据和这个条件最像的几个人最后都找了什么类型的对象。如果离得最近的5个人里有4个找了“温柔顾家型”1个找了“事业拼搏型”那你可以大胆预测这个人大概率也适合“温柔顾家型”。这就是KNN的思想。1.2 KNN的官方定义翻译成人话版KNN算法如果一个样本在特征空间中的K个最相似的样本中的大多数属于某一个类别那么这个样本也属于这个类别。一句话总结你邻居是啥样你就是啥样。1.3 KNN能解决什么问题问题类型说明举例分类问题预测“是哪一类”预测相亲结果是“成”还是“不成”回归问题预测“是多少”预测约会后“感情评分”是几分连续值KNN是少数既能做分类又能做回归的算法这点很硬核。第二部分KNN的“相似性”怎么算核心公式KNN里最重要的一步就是判断“谁和我最像”。怎么判断算距离。距离越近越相似。2.1 欧氏距离最常用的距离公式这就是你初中就学过的两点间距离公式只不过从二维推广到了多维。二维空间两个特征d(x1−x2)2(y1−y2)2三维空间三个特征d(x1−x2)2(y1−y2)2(z1−z2)2N维空间N个特征这是最通用的版本dk1∑n(x1k−x2k)22.2 拿“相亲匹配”算一遍距离假设我们把相亲对象用3个特征表示对象年龄年收入(万)身高(cm)匹配结果A2620175成功B3050180成功C2815170失败D2560185成功E3525172失败新来的X2835178现在我们要判断 X 属于“成功”还是“失败”。第一步计算 X 到每一个已知样本的距离以 X 到 A 为例三维欧氏距离d(28−26)2(35−20)2(178−175)242259238≈15.43dXA(28−26)2(35−20)2(178−175)242259238≈15.43同样的方法算出 X 到 B、C、D、E 的距离。第二步按距离从小到大排序排名对象距离匹配结果1B?成功2D?成功3A?成功4E?失败5C?失败第三步选K个最近的邻居投票决定假设K3取前3个最近的B成功、D成功、A成功→ 3票成功0票失败。结论KNN预测 X 的匹配结果是“成功”。第三部分KNN的分类流程和回归流程完整版3.1 分类流程5步走计算距离计算未知样本到每一个训练样本的距离欧氏距离排序将训练样本按距离从小到大排序取K个取出距离最近的 K 个训练样本投票表决统计这 K 个样本中哪个类别出现次数最多得出结论将未知样本归到出现次数最多的类别3.2 回归流程5步走计算距离计算未知样本到每一个训练样本的距离排序将训练样本按距离从小到大排序取K个取出距离最近的 K 个训练样本取平均值把这 K 个样本的目标值标签求平均得出结论这个平均值就是未知样本的预测值分类 vs 回归分类是“少数服从多数”投票回归是“邻居取平均”。第四部分K值怎么选决定模型生死的关键K值选得好模型就好K值选不好模型就废。4.1 K值过小比如 K1只找最近的一个邻居风险如果那个邻居是异常点噪音你就跟着跑偏了后果模型变得复杂容易过拟合4.2 K值过大比如 KNN是全部样本数所有邻居都算上看全体的意见风险如果数据里“成功”占多数永远预测“成功”忽略了局部信息后果模型变得太简单容易欠拟合4.3 实际工作中怎么选K一般K取较小的值3、5、7、9、11等奇数避免平票用交叉验证来确定最优K值后面会细讲第五部分手写KNN代码从0开始光说不练假把式。咱们用纯Python手写一个KNN分类器方便你理解底层原理。import numpy as np from collections import Counter class KNNClassifier: 手写一个KNN分类器帮你理解底层原理 def __init__(self, k5): self.k k self.X_train None self.y_train None def fit(self, X, y): 训练其实就是把数据存下来 self.X_train np.array(X) self.y_train np.array(y) def _euclidean_distance(self, x1, x2): 计算欧氏距离√(∑(x1-x2)²) return np.sqrt(np.sum((x1 - x2) ** 2)) def predict_one(self, x): 预测单个样本的分类 # 1. 计算x到所有训练样本的距离 distances [self._euclidean_distance(x, x_train) for x_train in self.X_train] # 2. 按距离从小到大排序取前k个的索引 k_indices np.argsort(distances)[:self.k] # 3. 取这k个样本的标签 k_labels [self.y_train[i] for i in k_indices] # 4. 投票表决多数取胜 most_common Counter(k_labels).most_common(1)[0][0] return most_common def predict(self, X): 批量预测 return [self.predict_one(x) for x in np.array(X)] # 测试我们的手写KNN # 造一个简单的数据集特征年龄年收入身高 X_train [[26, 20, 175], [30, 50, 180], [28, 15, 170], [25, 60, 185], [35, 25, 172]] y_train [成功, 成功, 失败, 成功, 失败] # 新来的一个相亲对象 X_test [[28, 35, 178]] # 用K3训练和预测 knn KNNClassifier(k3) knn.fit(X_train, y_train) result knn.predict(X_test) print(f预测结果{result[0]}) # 输出成功这段代码不依赖任何第三方库完完整整展示了KNN的5个步骤。你先把它跑通体会一下“距离→排序→取K个→投票”的逻辑。第六部分KNN算法的API用法实际开发中你不需要手写直接用sklearn库方便太多了。6.1 KNN分类KNeighborsClassifierfrom sklearn.neighbors import KNeighborsClassifier # 1. 准备数据 X [[0], [1], [2], [3]] # 特征比如学习时长 y [0, 0, 1, 1] # 标签0未通过1通过 # 2. 创建模型K1 model KNeighborsClassifier(n_neighbors1) # 3. 训练 model.fit(X, y) # 4. 预测 pred model.predict([[4]]) # 学习4小时能通过吗 print(pred) # 输出[1]通过6.2 KNN回归KNeighborsRegressorfrom sklearn.neighbors import KNeighborsRegressor # 1. 准备数据特征3个标签是连续值 X [[0, 0, 1], [1, 1, 0], [3, 10, 10], [4, 11, 12]] y [0.1, 0.2, 0.3, 0.4] # 连续值 # 2. 创建模型K2 model KNeighborsRegressor(n_neighbors2) # 3. 训练 model.fit(X, y) # 4. 预测 pred model.predict([[3, 11, 10]]) print(pred) # 输出取两个最近邻居的平均值6.3 分类和回归的API对比总结任务类型APIK值参数分类KNeighborsClassifier(n_neighbors5)n_neighbors回归KNeighborsRegressor(n_neighbors5)n_neighbors第七部分特征预处理归一化 vs 标准化7.1 为什么需要特征预处理假设你判断一个人“健康风险”用两个特征身高1.6m ~ 1.9m年收入5万 ~ 100万年收入的数值范围5~100比身高1.6~1.9大得多。在计算欧氏距离时年收入会“霸凌”身高——距离几乎由年收入决定身高的影响被淹没了。解决方法把所有特征都“压缩”到差不多的数值范围内。7.2 归一化MinMaxScaler公式X′(x-min)/(max-min)把数据映射到[0, 1]区间。from sklearn.preprocessing import MinMaxScaler data [[90, 2, 10, 40], [60, 4, 15, 45], [75, 3, 13, 46]] scaler MinMaxScaler() data_scaled scaler.fit_transform(data) print(data_scaled)缺点如果数据里有异常值比如某个特征有个1000最大值被拉大其他所有值都被压到特别小。鲁棒性差。7.3 标准化StandardScaler——推荐使用公式X′(x−mean)/标准差把数据转换成均值为0标准差为1的标准正态分布。from sklearn.preprocessing import StandardScalerdata [[90, 2, 10, 40], [60, 4, 15, 45], [75, 3, 13, 46]] scaler StandardScaler() data_scaled scaler.fit_transform(data) print(data_scaled) print(均值:, scaler.mean_) print(方差:, scaler.var_)优点即使有异常值因为数据量足够大对均值和标准差的影响也很有限。鲁棒性强。7.4 归一化 vs 标准化 怎么选对比维度归一化标准化鲁棒性抗异常值❌ 差✅ 好适用场景传统精确小数据现代嘈杂大数据推荐是否改变数据分布会压缩成[0,1]变成标准正态分布实战建议无脑选标准化StandardScaler除非你有特殊理由。7.5 小练习课件里有个填空我帮你填上归一化度量值容易受到样本中最大值和最小值的影响鲁棒性差。在样本数量较大的情况下异常值对样本的均值和标准差的影响可以忽略不计。第八部分完整实战——鸢尾花分类KNN标准流程终于到了一个完整的机器学习项目了咱们用KNN做鸢尾花分类走一遍完整建模流程。8.1 数据长什么样鸢尾花数据集有150个样本3个品种山鸢尾、变色鸢尾、维吉尼亚鸢尾每个样本有4个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度标签是花的品种0、1、2。8.2 代码实现# 导入所有需要的库 from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score # 第1步获取数据 iris load_iris() print(f特征数量: {iris.data.shape[1]}, 样本数量: {iris.data.shape[0]}) print(f目标类别: {iris.target_names}) # [setosa versicolor virginica] # 第2步数据基本处理划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( iris.data, iris.target, test_size0.2, random_state22 ) print(f训练集: {len(X_train)}个, 测试集: {len(X_test)}个) # 第3步特征工程 —— 标准化重要 scaler StandardScaler() # 用训练集拟合scaler然后转换训练集 X_train_scaled scaler.fit_transform(X_train) # 用同样的scaler转换测试集注意这里只用transform不用fit X_test_scaled scaler.transform(X_test) # 第4步机器学习模型训练 # 创建KNN分类器K3 model KNeighborsClassifier(n_neighbors3) model.fit(X_train_scaled, y_train) # 第5步模型评估 # 方法1直接用score方法算准确率 accuracy model.score(X_test_scaled, y_test) print(f模型准确率: {accuracy * 100:.2f}%) # 方法2先预测再算准确率结果一样 y_pred model.predict(X_test_scaled) acc accuracy_score(y_test, y_pred) print(f准确率(第二种方法): {acc * 100:.2f}%) # 第6步模型预测新数据 # 假设来了两朵新花测量了4个特征值 new_flowers [[5.1, 3.5, 1.4, 0.2], [6.7, 3.0, 5.2, 2.3]] # 注意新数据也要做同样的标准化处理 new_flowers_scaled scaler.transform(new_flowers) pred model.predict(new_flowers_scaled) print(f预测类别: {pred}) # 输出[0 2] 分别对应山鸢尾和维吉尼亚鸢尾 # 看预测概率每个类别的概率 prob model.predict_proba(new_flowers_scaled) print(f预测概率: {prob})8.3 运行结果分析特征数量: 4, 样本数量: 150 目标类别: [setosa versicolor virginica] 训练集: 120个, 测试集: 30个 模型准确率: 100.00% 准确率(第二种方法): 100.00% 预测类别: [0 2] 预测概率: [[1. 0. 0. ] [0. 0. 1. ]]第一个新花100%概率是类别0山鸢尾第二个100%概率是类别2维吉尼亚鸢尾。第九部分交叉验证Cross Validation—— 模型评估更靠谱9.1 为什么需要交叉验证你之前是一次性划分训练集和测试集这样有个风险如果这次划分恰好把难的数据都分到了测试集评估结果就偏低如果恰好把简单的都分到了测试集评估结果就虚高。交叉验证把数据分成N份每份轮流做一次测试集其他N-1份做训练集训练N次取平均分。9.2 交叉验证的流程以5折交叉验证为例把训练集分成5份每份叫一折第1次第1份做验证集其他4份做训练集 → 得一个分数第2次第2份做验证集其他4份做训练集 → 得一个分数...以此类推共5次取5次得分的平均值作为最终分数好处模型评分更稳定、更可信不容易被“运气好的一次划分”忽悠。第十部分网格搜索Grid Search—— 自动找最优K值10.1 为什么需要网格搜索KNN里的 K值邻居个数是超参数——它不是从数据里学出来的是你人为设定的。你可能会想K3好还是K5好K7会不会更好一个个手动试太累了。网格搜索你告诉它“我要试哪些K值”它自动帮你全部试完交叉验证打分返回最好的那个。10.2 网格搜索 交叉验证 代码from sklearn.model_selection import GridSearchCV # 准备数据和之前一样 iris load_iris() X_train, X_test, y_train, y_test train_test_split( iris.data, iris.target, test_size0.2, random_state22 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 创建KNN模型 knn KNeighborsClassifier() # 设置要搜索的超参数列表 param_grid {n_neighbors: [1, 3, 5, 7, 9, 11]} # 网格搜索 5折交叉验证 grid_search GridSearchCV( estimatorknn, # 要调参的模型 param_gridparam_grid, # 要搜索的参数组合 cv5, # 5折交叉验证 n_jobs-1 # 用所有CPU核加速 ) grid_search.fit(X_train_scaled, y_train) # 查看结果 print(f最佳K值: {grid_search.best_params_}) print(f交叉验证最佳得分: {grid_search.best_score_:.4f}) print(f最优模型: {grid_search.best_estimator_}) print(f测试集准确率: {grid_search.score(X_test_scaled, y_test):.4f}) # 查看所有搜索结果转成DataFrame方便看 import pandas as pd cv_results pd.DataFrame(grid_search.cv_results_) print(cv_results[[param_n_neighbors, mean_test_score, std_test_score]])10.3 运行结果最佳K值: {n_neighbors: 3} 交叉验证最佳得分: 0.9667 最优模型: KNeighborsClassifier(n_neighbors3) 测试集准确率: 1.0000 param_n_neighbors mean_test_score std_test_score 0 1 0.966667 0.033333 1 3 0.966667 0.033333 2 5 0.958333 0.030000 3 7 0.958333 0.030000 4 9 0.958333 0.030000 5 11 0.958333 0.030000可以看到K1和K3在交叉验证上得分相同但K3可能更稳健不易受异常值影响所以选K3。第十一部分分类评估方法混淆矩阵、精确率、召回率、F111.1 为什么只看准确率不够课件里用了一个特别好的例子癌症检测。假设10000个人里只有10个癌症患者正例。如果一个模型把所有10000个人都预测为“健康”它的准确率是99901000099.9%10000999099.9%准确率99.9%看起来牛逼炸了但实际上一个癌症患者都没检测出来这模型就是个废物。所以对于类别不平衡的数据需要更多指标来评估。11.2 混淆矩阵Confusion Matrix混淆矩阵是一个 2x2 的表格帮我们看清楚“预测对了多少错在哪”。二分类问题正例 癌症患者反例 健康人预测为正例预测为反例真实为正例TP真正例正确识别患者FN假反例漏诊了患者真实为反例FP假正例误把健康人当患者TN真反例正确识别健康人TPTrue Positive真实是正例预测也是正例 ✅FNFalse Negative真实是正例预测是反例 ❌漏诊最可怕FPFalse Positive真实是反例预测是正例 ❌误诊TNTrue Negative真实是反例预测也是反例 ✅11.3 精确率Precision—— 查准率精确率TPFPTP翻译模型说“你是癌症患者”的人里面真正是患者的比例。如果你不想把健康人误诊为癌症误诊造成恐慌你就追求高精确率。11.4 召回率Recall—— 查全率 / 敏感度召回率TPFNTP翻译所有真正的癌症患者里面模型找出了多少比例。如果你不想漏掉任何一个癌症患者漏诊耽误病情你就追求高召回率。11.5 精确率 vs 召回率 —— 一个经典的矛盾模型TPFNFPTN精确率召回率模型A保守3304100%50%模型B激进603167%100%模型A宁可少预测只预测了3个阳性但预测的都对精确率100%。缺点是漏掉了3个患者召回率低。模型B把所有10个样本都预测为阳性6个真正的患者全找到了召回率100%。缺点是误伤了4个健康人精确率低。没有绝对的好坏看业务需求。癌症筛查宁可误诊也不能漏诊所以优先保证召回率。11.6 F1-score —— 精确率和召回率的“调和平均”F1精确率召回率2×精确率×召回率F1-score 是一个综合指标同时考虑了精确率和召回率。如果精确率和召回率都很高F1才高任何一个拉胯F1就掉下来了。模型A精确率100%召回率50%F11.00.52×1.0×0.51.51.0≈0.67模型B精确率67%召回率100%F10.671.02×0.67×1.01.671.34≈0.80模型B的F1更高说明综合考虑下模型B更好。11.7 代码算混淆矩阵、精确率、召回率、F1from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score # 真实的10个样本标签6个恶性正例4个良性反例 y_true [恶性, 恶性, 恶性, 恶性, 恶性, 恶性, 良性, 良性, 良性, 良性] # 模型A的预测对了3个恶性4个良性 y_pred_A [恶性, 恶性, 恶性, 良性, 良性, 良性, 良性, 良性, 良性, 良性] print( 模型A ) print(混淆矩阵:) print(confusion_matrix(y_true, y_pred_A, labels[恶性, 良性])) print(f精确率: {precision_score(y_true, y_pred_A, pos_label恶性):.2f}) print(f召回率: {recall_score(y_true, y_pred_A, pos_label恶性):.2f}) print(fF1-score: {f1_score(y_true, y_pred_A, pos_label恶性):.2f}) # 模型B的预测对了6个恶性1个良性 y_pred_B [恶性, 恶性, 恶性, 恶性, 恶性, 恶性, 恶性, 恶性, 良性, 良性] print(\n 模型B ) print(混淆矩阵:) print(confusion_matrix(y_true, y_pred_B, labels[恶性, 良性])) print(f精确率: {precision_score(y_true, y_pred_B, pos_label恶性):.2f}) print(f召回率: {recall_score(y_true, y_pred_B, pos_label恶性):.2f}) print(fF1-score: {f1_score(y_true, y_pred_B, pos_label恶性):.2f})输出 模型A 混淆矩阵: [[3 3] [0 4]] 精确率: 1.00 召回率: 0.50 F1-score: 0.67 模型B 混淆矩阵: [[6 0] [3 1]] 精确率: 0.67 召回率: 1.00 F1-score: 0.80第十二部分总结模块核心内容算法思想“近朱者赤近墨者黑”——看最近的K个邻居相似度度量欧氏距离 d∑(x1−x2)²K值太小过拟合太大欠拟合用交叉验证选最优分类K个邻居投票多数取胜回归K个邻居取平均特征预处理必须做标准化让特征在同一尺度交叉验证把数据分N份轮流做验证集取平均分网格搜索自动尝试多组超参数返回最优组合混淆矩阵TP、FN、FP、TN四个指标看清模型精确率预测为正例里面真正为正例的比例召回率真正为正例里面被预测出来的比例F1-score精确率和召回率的调和平均综合评价KNN的代码写起来就几行但背后的逻辑——距离怎么算、K怎么选、特征要不要标准化、怎么评估模型好不好——这些才是真正的功力。今天把这些问题都搞懂了以后学其他算法就会轻松很多。明天咱们继续