尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

KNN算法实战:从电影推荐到房价预测与手写识别

KNN算法实战:从电影推荐到房价预测与手写识别 1. 从“人以群分”到“物以类聚”KNN算法的生活化理解我们每天其实都在不自觉地使用一种算法只是我们自己没意识到。比如你搬到一个新小区想找个靠谱的理发店你会怎么做大概率是问问邻居或者看看哪家店门口排队的人多。再比如你看到一种没见过的水果想知道它甜不甜你可能会观察它和哪种你熟悉的水果长得最像。这些判断背后的逻辑本质上就是K最近邻算法也就是我们常说的KNN算法。KNNK-Nearest Neighbors是机器学习中最直观、最“懒惰”的算法之一。说它直观是因为它的核心思想就是“近朱者赤近墨者黑”——一个未知事物的类别由它周围最相似的K个已知事物的类别投票决定。说它“懒惰”是因为它不像其他算法比如神经网络那样需要经历一个复杂的“训练”过程去调整内部参数它只是简单地把所有已知数据记下来等到需要做预测时才临时去计算距离、找邻居。这种特性让它特别适合作为入门机器学习的第一个算法也特别适合用来解决我们生活中那些基于相似度判断的问题。很多人学KNN止步于理解公式和调用sklearn库但真正要把它用起来尤其是想用在一些生活化的场景里会遇到一堆教科书上不会讲的细节K值选3还是选5凭感觉吗数据里身高是1.8米收入是8000元直接算距离公平吗万一邻居们“打平手”了怎么办这些问题不解决算法就跑不起来或者跑出来的结果根本不可信。这篇文章我就以一个从业者的角度抛开复杂的数学推导用几个你身边触手可及的案例手把手带你实现KNN并把上面这些“坑”一个个填平。我们会从给电影分类、到预测房价、再到识别手写数字把KNN里里外外摸个透。你会发现实现一个算法不难但让一个算法在真实场景中可靠地工作才是真正的功夫。2. 案例一构建你的电影推荐小系统我们先从一个有趣的场景开始电影推荐。假设你和朋友们有一个观影记录表记录了每个人对若干部电影的打分1-5分。现在有一部新电影《星际穿越》你还没看但想知道自己会不会喜欢。一个很自然的想法是看看那些和你口味最相似的朋友们对它的评价。2.1 数据准备与“距离”的定义首先我们需要数据。这里每个人的观影记录可以看作一个多维空间中的点。例如我们只考虑三部电影《盗梦空间》、《泰坦尼克号》、《复仇者联盟》。人名《盗梦空间》评分《泰坦尼克号》评分《复仇者联盟》评分对新电影《星际穿越》的评分标签小明5144 (喜欢)小红4522 (不喜欢)小刚2455 (喜欢)小强125? (待预测)我们的目标是根据小强对前三部电影的评分1 2 5预测他对《星际穿越》的评分倾向。核心问题来了如何定义“口味相似”在数学上就是计算“距离”。最常用的就是欧几里得距离也就是我们中学学的两点间直线距离。把小强和小明看作三维空间的两个点小强坐标: (1 2 5)小明坐标: (5 1 4)他们之间的距离是sqrt((1-5)^2 (2-1)^2 (5-4)^2) sqrt(16 1 1) sqrt(18) ≈ 4.24同理我们可以计算出小强与小红、小刚的距离。距离越小说明两人的观影口味越相似。注意这里埋下了第一个坑。我们的评分尺度是1-5看起来一致但如果我们加入另一个特征比如“观影次数”可能高达几百次那么计算距离时“观影次数”的数值影响会远远大于“评分”这会导致距离计算被某一个特征“主导”。这就是为什么在实际应用中数据标准化是必不可少的一步。我们会在后面的房价案例中详细处理。2.2 K值选择与投票决策假设我们计算出小强与三人的距离从小到大依次是小刚3.0、小明4.24、小红4.58。现在我们要选择K值。K就是我们要参考的“邻居”数量。如果K1我们只看最近的一个邻居也就是小刚。小刚对《星际穿越》的评分是5喜欢所以我们预测小强也会喜欢。如果K3我们看全部三个邻居。他们的评分分别是小刚(喜欢)、小明(喜欢)、小红(不喜欢)。进行投票喜欢票2张不喜欢票1张所以我们预测小强会喜欢。你看K值不同结论可能相同也可能不同。K值的选择没有黄金标准但有一些经验法则通常取奇数为了避免平票情况比如K2时两个邻居意见相反。K值不宜过大或过小K太小如1模型容易受到噪声数据某个邻居的异常偏好的影响变得不稳定K太大则会包含进许多实际上并不相似的“远邻”导致预测模糊失去本地化特征的意义。常用方法是交叉验证将已知数据的一部分作为训练集另一部分作为测试集尝试不同的K值比如1 3 5 7...看哪个K值在测试集上的预测准确率最高。在这个简单例子里数据量小我们可以直观地选K3。2.3 Python代码实现下面我们用Python的scikit-learn库和纯手工计算两种方式来实现这个预测。# 方式一使用scikit-learn库生产环境推荐 import numpy as np from sklearn.neighbors import KNeighborsClassifier # 训练数据前三部电影的评分 X_train np.array([ [5 1 4] # 小明 [4 5 2] # 小红 [2 4 5] # 小刚 ]) # 标签对《星际穿越》的喜好这里简化为二分类4/5为喜欢1 2为不喜欢0 y_train np.array([1 0 1]) # 小明(喜欢)小红(不喜欢)小刚(喜欢) # 待预测的数据小强的评分 X_test np.array([[1 2 5]]) # 创建KNN分类器设置K3 knn KNeighborsClassifier(n_neighbors3) knn.fit(X_train y_train) # “训练”模型其实就是记住数据 # 进行预测 prediction knn.predict(X_test) prediction_proba knn.predict_proba(X_test) # 查看属于每个类别的概率 print(f使用sklearn预测结果类别: {prediction[0]} (0:不喜欢 1:喜欢)) print(f预测概率: {prediction_proba[0]}) # 例如 [0.333 0.667] 表示不喜欢概率33.3%喜欢概率66.7% # 方式二手动计算帮助理解原理 def euclidean_distance(a b): 计算两个点之间的欧几里得距离 return np.sqrt(np.sum((a - b) ** 2)) # 计算小强与每个邻居的距离 distances [] for i person in enumerate(X_train): dist euclidean_distance(X_test[0] person) distances.append((dist y_train[i])) # 存储距离 标签 # 按距离排序取前K3个 distances.sort(keylambda x: x[0]) k_nearest distances[:3] # 统计K个邻居中各类别的票数 from collections import Counter votes Counter([label for (_, label) in k_nearest]) print(f\n手动计算最近的3个邻居及其标签: {k_nearest}) print(f投票结果: {votes}) print(f预测类别: {votes.most_common(1)[0][0]})运行这段代码你会看到两种方式都得出了小强“喜欢”的预测结果。手动计算的过程清晰地展示了KNN算法的每一步计算距离、排序、取前K个、投票。实操心得在真实推荐系统中用户-物品评分矩阵非常庞大且稀疏一个人只看过极少部分电影。直接使用KNN计算用户间距离称为“User-CF”效率很低。更常见的做法是使用更高效的协同过滤算法或者对矩阵进行降维。但KNN的思想是这些高级方法的基础理解它至关重要。3. 案例二房价预测中的“相似房源”逻辑第二个案例我们升级难度处理一个经典的回归问题预测房价。假设你是房产中介有一套新房源面积120平米 3居室 房龄10年你想给它估个价。一个很朴素的业务逻辑就是在历史成交记录里找几套和它最像的房子看看它们都卖了多少钱取个平均数或者中位数。这就是KNN回归。3.1 数据标准化为什么它是成败关键我们构建一个简单的数据集房源面积(平米)居室数房龄(年)成交价(万元)A8025320B10038450C150420600D90312380E120310? (待预测)现在我们想为房源E120 3 10预测价格。直接计算欧氏距离试试看特征1面积 数值范围80-150。特征2居室数 数值范围2-4。特征3房龄 数值范围5-20。计算E与A的距离sqrt((120-80)^2 (3-2)^2 (10-5)^2) sqrt(1600 1 25) sqrt(1626) ≈ 40.32发现问题了吗距离的计算主要被“面积”这个特征主导了因为它的数值变化范围最大70而“居室数”的变化范围只有2贡献微乎其微。这意味着在算法眼里两套房子是否相似几乎只取决于面积差得大不大居室和房龄的影响被严重忽略了。这显然不符合我们的常识一套120平的三居和一套80平的两居即使面积差40平也可能因为都是紧凑刚需户型而价格有可比性但一套120平的三居和一套150平的四居虽然面积只差30平但定位可能完全不同。为了解决这个问题我们必须进行特征标准化将所有特征缩放到同一个尺度上。最常用的方法是Z-score标准化也叫标准差标准化(原始值 - 均值) / 标准差。这样处理后的数据每个特征的均值变为0标准差变为1分布形状不变。import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler # 原始数据 data { 面积: [80 100 150 90] 居室: [2 3 4 3] 房龄: [5 8 20 12] 价格: [320 450 600 380] } df pd.DataFrame(data) features df[[面积 居室 房龄]] target df[价格] # 待预测的房源E E np.array([[120 3 10]]) # 1. 标准化处理非常重要 scaler StandardScaler() features_scaled scaler.fit_transform(features) # 拟合训练数据并转换 E_scaled scaler.transform(E) # 用同样的标准转换待预测数据 print(标准化后的特征数据训练集:) print(features_scaled) print(f\n标准化后的房源E特征:) print(E_scaled) # 查看标准化后的均值和标准差验证是否约为0和1 print(f\n标准化后各特征均值: {np.mean(features_scaled axis0)}) print(f标准化后各特征标准差: {np.std(features_scaled axis0)})经过标准化面积、居室、房龄这三个特征现在处于同一数量级在计算距离时拥有了同等的话语权。3.2 KNN回归的实现与预测KNN用于回归时决策方式不是投票而是对K个邻居的标签这里是价格取平均值或中位数。通常取平均值。from sklearn.neighbors import KNeighborsRegressor # 创建KNN回归模型设置K2 knn_reg KNeighborsRegressor(n_neighbors2) knn_reg.fit(features_scaled target) # 使用标准化后的特征进行训练 # 预测房源E的价格 predicted_price knn_reg.predict(E_scaled) print(f\n使用KNN回归K2预测的房源E价格: {predicted_price[0]:.2f} 万元) # 我们可以手动验证一下 # 计算标准化后E与所有房源的距离 from sklearn.metrics.pairwise import euclidean_distances dists euclidean_distances(E_scaled features_scaled) print(f\n房源E与各房源的标准欧氏距离: {dists[0]}) # 找到距离最近的两个邻居索引 nearest_indices np.argsort(dists[0])[:2] print(f最近的2个邻居索引: {nearest_indices}) print(f对应的房源: {df.iloc[nearest_indices][[面积 居室 房龄 价格]].values}) print(f两个邻居的价格: {target.iloc[nearest_indices].values}) print(f价格平均值: {target.iloc[nearest_indices].mean():.2f})运行代码你会看到模型输出了一个预测价格。手动计算也验证了预测结果就是最近两个邻居房价的简单平均。避坑指南在实际房价预测中仅仅使用面积、居室、房龄是远远不够的。地段可转化为经纬度或行政区划编码、楼层、朝向、装修情况、学区、地铁距离等都是重要特征。处理这些特征需要更多的技巧类别特征如“朝向”东、南、西、北不能直接代入计算距离。需要将其转换为数值常用方法是独热编码为每个类别创建一个新的0/1特征列。文本特征如“小区名称”需要更复杂的自然语言处理或直接舍弃。特征权重并非所有特征都同等重要。你可以通过业务知识如认为地段比房龄重要得多或算法如使用互信息法、基于模型的特征重要性来赋予不同特征不同的权重在计算距离时体现出来。这属于特征工程的范畴是提升模型性能的关键。4. 案例三手写数字识别——图像的本质是数据最后我们挑战一个更经典的机器学习任务识别手写数字。这听起来很高大上但用KNN来实现其核心思想却异常简单。我们使用著名的MNIST数据集简化版来演示。4.1 图像数据的向量化计算机不认识图片它只认识数字。一张灰度手写数字图片比如一个8x8像素的小图本质上就是一个8行8列的矩阵每个格子像素有一个灰度值0-255 0代表纯黑255代表纯白。一张手写数字“3”的8x8像素矩阵示例简化 [[ 0. 0. 5. 13. 9. 1. 0. 0.] [ 0. 0. 13. 15. 10. 15. 5. 0.] [ 0. 3. 15. 2. 0. 11. 8. 0.] ... [ 0. 4. 12. 0. 0. 7. 8. 0.] [ 0. 5. 16. 10. 0. 16. 6. 0.] [ 0. 0. 6. 15. 13. 2. 0. 0.]]我们要做的第一步就是把这个二维的8x8矩阵“拉平”成一个一维的、长度为648*8的向量。这个向量就是这张图片在64维空间中的一个“点”数据集中有成千上万个这样的点向量每个点都有一个标签0-9代表它是数字几。KNN要做的就是当有一个新的、未知的手写数字图片也是一个64维的点出现时在已知的成千上万个点里找到和它“距离”最近的K个点然后看这K个点大部分是哪个数字就判定新图片也是那个数字。4.2 代码实现与性能观察我们使用sklearn内置的小型MNIST数据集load_digits来演示。from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score classification_report import matplotlib.pyplot as plt import numpy as np # 1. 加载数据 digits load_digits() X y digits.data digits.target # X是已经拉平为64维向量的图像数据y是对应的数字标签 print(f数据集形状: {X.shape}) # 应该输出 (1797 64)表示1797张图片每张64个特征像素 print(f标签形状: {y.shape}) print(f一个样本的数据前10个像素值: {X[0][:10]}...) print(f对应标签: {y[0]}) # 可视化第一张图片 plt.figure(figsize(44)) plt.imshow(X[0].reshape(8 8) cmapgray) # 将64维向量重塑回8x8矩阵显示 plt.title(fLabel: {y[0]}) plt.axis(off) plt.show() # 2. 分割数据集训练集和测试集 X_train X_test y_train y_test train_test_split(X y test_size0.2 random_state42) print(f\n训练集样本数: {X_train.shape[0]} 测试集样本数: {X_test.shape[0]}) # 3. 创建并训练KNN模型 # 注意图像像素值已经是0-16的尺度且所有特征像素理论上同等重要这里可以不做标准化但做了也无害。 knn_digits KNeighborsClassifier(n_neighbors5) # 先尝试K5 knn_digits.fit(X_train y_train) # 4. 在测试集上进行预测并评估 y_pred knn_digits.predict(X_test) accuracy accuracy_score(y_test y_pred) print(f\n模型在测试集上的准确率: {accuracy:.4f}) print(\n详细分类报告:) print(classification_report(y_test y_pred)) # 5. 看看哪些预测错了分析 errors (y_pred ! y_test) if errors.any(): print(f\n共有 {errors.sum()} 个预测错误的样本。) # 随机看几个错误案例 error_indices np.where(errors)[0] for i in error_indices[:3]: # 只看前3个错误 plt.figure(figsize(62)) plt.subplot(121) plt.imshow(X_test[i].reshape(88) cmapgray) plt.title(fTrue: {y_test[i]} Pred: {y_pred[i]}) plt.axis(off) # 找出它的5个最近邻居 distances indices knn_digits.kneighbors([X_test[i]]) plt.subplot(122) # 这里可以展示邻居图片代码略复杂暂不展开 plt.show()运行这段代码你会得到一个准确率通常在95%以上。这意味着对于一个你从未见过的手写数字这个简单的KNN模型有95%以上的概率能认对。这已经是一个非常不错的结果了4.3 探索K值对准确率的影响K值的选择在这里尤为重要。我们可以通过一个循环来观察。# 探索不同K值对准确率的影响 k_range range(1 16) train_accuracy [] test_accuracy [] for k in k_range: knn KNeighborsClassifier(n_neighborsk) knn.fit(X_train y_train) train_accuracy.append(knn.score(X_train y_train)) test_accuracy.append(knn.score(X_test y_test)) plt.figure(figsize(106)) plt.plot(k_range train_accuracy labelTraining Accuracy) plt.plot(k_range test_accuracy labelTesting Accuracy) plt.xlabel(Value of K for KNN) plt.ylabel(Accuracy) plt.title(K值对训练集和测试集准确率的影响) plt.legend() plt.grid(True) plt.show() # 找到测试集上准确率最高的K值 best_k k_range[np.argmax(test_accuracy)] print(f在测试集上表现最好的K值是: {best_k} 准确率为: {max(test_accuracy):.4f})绘制出的曲线通常会显示当K1时训练准确率100%因为每个点最近的邻居就是它自己但测试准确率并非最高说明模型可能“过拟合”了对噪声太敏感。随着K增大训练准确率下降测试准确率先上升后下降。那个测试准确率的峰值点往往就是我们想要的K值。性能瓶颈与优化思考KNN在这个案例中表现良好但它有两个致命缺点计算效率低预测时需要计算待测样本与所有训练样本的距离。当训练集有上百万样本时预测速度会慢得无法接受。解决方案包括使用KD-Tree、Ball Tree等数据结构来加速近邻搜索或者对数据进行降维如PCA。存储开销大需要保存整个训练集。对于大数据集内存消耗巨大。对不相关特征和尺度敏感正如房价案例所示必须做特征标准化。对于图像虽然像素尺度一致但如果图片背景复杂、数字位置不居中效果会大打折扣。因此在实际的OCR光学字符识别中预处理二值化、去噪、归一化、居中比模型本身更重要。5. 从实现到优化KNN的实战经验总结走完三个案例你应该已经能亲手实现KNN算法并理解其核心思想了。但在真正的项目里让KNN发挥出最佳效果还需要考虑更多。下面分享几个教科书里不常提但至关重要的实战经验。5.1 距离度量不止欧氏距离一种选择我们一直用的是欧氏距离它很直观但并非放之四海而皆准。曼哈顿距离在网格状道路的城市里如纽约曼哈顿两点间距离是沿街行走的距离而不是直线。公式为各维度坐标差绝对值的和。它对异常值的敏感度低于欧氏距离。余弦相似度衡量的是两个向量在方向上的差异而不是绝对距离。在文本分类、推荐系统中极其常用。比如比较两篇文章的相似度我们更关心词频向量的角度主题是否相似而不是它们的长度文章总词数。闵可夫斯基距离欧氏距离和曼哈顿距离的泛化形式。当特征高度相关时可以考虑使用马氏距离它能考虑特征间的相关性。在sklearn的KNeighborsClassifier中通过metric参数可以轻松切换。# 使用曼哈顿距离 knn_manhattan KNeighborsClassifier(n_neighbors5 metricmanhattan) # 使用余弦相似度注意sklearn的‘cosine’度量计算的是余弦距离即1-余弦相似度 knn_cosine KNeighborsClassifier(n_neighbors5 metriccosine)如何选择没有定论。一个可靠的方法是将距离度量作为超参数连同K值一起通过交叉验证网格搜索来选择最佳组合。5.2 权重邻居的“话语权”可以不同标准的KNN投票是“一人一票”。但直觉告诉我们距离更近的邻居应该比稍远的邻居拥有更大的话语权。我们可以引入距离权重。在sklearn中设置weightsdistance即可。此时每个邻居的投票权重为其距离的倒数或类似函数。距离越近权重越大。这在很多场景下能提升模型性能尤其是当数据分布不均匀时。knn_weighted KNeighborsClassifier(n_neighbors5 weightsdistance) knn_weighted.fit(X_train y_train)5.3 处理平票与多分类问题当K为偶数且两类票数相等时或者在多分类问题中出现多个类别票数并列第一时需要解决平票问题。sklearn的默认策略是weightsuniform时选择排序靠前的邻居所属的类别即按训练集索引顺序。你也可以通过实现自定义函数来处理但通常选择奇数K值就能有效避免。5.4 算法效率当数据量变大时当训练样本数N很大特征维度D也很高时暴力计算所有距离称为algorithmbrute的复杂度是O(N*D)会非常慢。此时应使用更快的算法algorithmkd_tree适用于低维D 20数据。它通过构建二叉树来分割空间将搜索复杂度降至O(D*logN)。algorithmball_tree适用于更高维的数据比KD-Tree更能处理复杂的距离度量。algorithmauto让sklearn根据数据自动选择最合适的算法。# 对于大型数据集使用ball_tree knn_fast KNeighborsClassifier(n_neighbors5 algorithmball_tree metricminkowski)5.5 一个综合的模型选择流程在实际项目中我通常会遵循以下步骤来应用KNN数据预处理处理缺失值将类别特征进行独热编码对数值特征进行标准化StandardScaler或归一化MinMaxScaler。划分数据集严格区分训练集、验证集和测试集。网格搜索交叉验证使用GridSearchCV在验证集上搜索最佳的超参数组合包括n_neighbors如1 3 5 ... 21、weightsuniformdistance、metriceuclideanmanhattanminkowski以及p参数当metricminkowski时p2为欧氏距离p1为曼哈顿距离。用最佳参数重新训练用找到的最佳参数在整个训练集训练验证上重新训练模型。最终评估在从未参与过任何训练的测试集上评估模型性能得到最终可信的准确率。分析错误查看在测试集上预测错误的样本尝试理解模型为什么出错是数据质量问题还是特征不够或者是KNN本身就不适合这类问题例如决策边界非常复杂的问题可能更适合神经网络。KNN就像一把瑞士军刀简单、直观、无需训练在数据量不大、特征维度不高、且决策边界不太复杂的场景下往往能快速给出一个不错的基线结果。它的预测过程透明易于向业务方解释——“因为这几个历史案例和你的情况最像所以推荐这个”。这种可解释性在当今复杂的黑盒模型时代反而成为它独特的优势。下次当你遇到一个基于相似度判断的问题时不妨先试试KNN它可能会给你一个惊喜。
返回列表