资讯详情

资讯详情

用纯Numpy手写KNN:鸢尾花数据分类与分析实战

1. 为什么用Numpy做鸢尾花分析而不是直接上Pandas先交代一下背景。鸢尾花数据集大概是机器学习里最著名的入门数据了150条样本、4个特征、3个类别简直是为教学量身定做的。市面上绝大多数教程会用Pandas做数据处理、用Scikit-learn做分类几行代码跑完就出结果。但如果你真的想搞懂数据是怎么被“喂”给模型的想理解分类算法内部到底做了什么我强烈建议你用纯Numpy从头到尾走一遍。有人可能会问Pandas不香吗Scikit-learn不香吗香当然香。但Pandas是基于Numpy构建的它的DataFrame底层就是Numpy数组的封装Scikit-learn同样依赖Numpy数组作为标准输入。换句话说Numpy是这套技术栈的地基。用Numpy亲手完成鸢尾花数据的加载、清洗、统计分析、特征筛选再到实现一个KNN分类器你会对“数据到底以什么形态存在”“距离是怎么算的”“类别是怎么投票出来的”这些问题有切身的体感。这份体感是直接用高级库换不来的。这个项目适合三类人刚学完Python基础、想接触数据分析的初学者想把Numpy用熟练、理解广播机制和向量化思维的人以及想搞懂KNN算法底层原理、不满足于调包的同学。整个项目不需要GPU、不需要分布式环境一台普通电脑、一个Python解释器就能跑完。我建议把这份代码当作一个练手模板跑通之后再往里面加自己的分析逻辑会有很多意外的收获。2. 数据加载与“用Numpy的方式”做数据探索2.1 数据集里到底装了什么鸢尾花数据集一共150条样本每条样本有4个特征花萼长度sepal length、花萼宽度sepal width、花瓣长度petal length、花瓣宽度petal width单位都是厘米。类别有3种山鸢尾Setosa、变色鸢尾Versicolor、维吉尼亚鸢尾Virginica每种50条。如果你没有现成的环境变量和内置数据最省事的加载方式是直接从Scikit-learn里取但那样其实已经“作弊”了因为拿到的直接就是Numpy数组和标签。我建议改用另一种方式去网上下载原始的CSV文件然后自己写一个加载函数。这样做的好处有两个一是你会明白数据在磁盘上的原始形态长什么样二是你被迫去处理字符串标签到数值标签的映射问题这正是真实工程中几乎必遇到的环节。2.2 手写一个CSV加载函数CSV文件本身是纯文本每一行是一条样本逗号分隔各个字段。用Numpy加载CSV通常有一条捷径np.genfromtxt或者np.loadtxt。但对于鸢尾花这种最后一列是字符串标签的数据直接用np.loadtxt会报错因为整个数组要求是同一种数据类型。解决方案是先读取文本把特征部分和标签部分拆开处理。我当时的做法是这样先用Python内置的csv模块读取全部行把特征数据存成列表的列表同时单独维护一个标签列表。然后调用np.array(features, dtypefloat)转成浮点数组标签则手工映射成整数——0代表Setosa1代表Versicolor2代表Virginica。整个过程大约十几行代码但它逼着我想清楚一个问题Numpy数组所有元素必须同类型你不可能在一个float64数组里混着存一个字符串的“Iris-setosa”。有一点值得留意np.genfromtxt其实是支持按列处理异构数据的比如用dtypeNone可以让它自动推断每一列的类型但那样返回的是一个结构化数组访问列的时候需要用字段名。对于这个项目来说结构化数组反而增加了复杂度在我看来不如手动分开处理来得直白。2.3 动手之前先看一眼数据长什么样数据加载完不要急着算均值方差先做两件事看shape和看前几行。import numpy as np # 假设 features 是 (150, 4) 的浮点数组labels 是 (150,) 的整数数组 print(features.shape) # (150, 4) print(labels.shape) # (150,) print(features[:5]) # 前5条样本 print(np.unique(labels, return_countsTrue))np.unique配合return_countsTrue可以一眼确认三个类别的样本数是否均衡。这个习惯在真实项目中很重要类别不均衡会直接导致后续模型偏向多数类。鸢尾花数据集三类各50条非常均衡但这不代表你可以跳过这一步——养成先看分布、再动手分析的习惯会让你在后面处理真实数据时少踩很多坑。3. 核心统计分析均值、方差、相关系数与Numpy向量化3.1 三类样本的均值差异有多大既然要分析鸢尾花数据集第一步自然是看不同类别在四个特征上的统计差异。最简单的方式是分组求均值。纯Python写三重循环也能求出来但那个速度放在大数据集上根本没法看。Numpy的向量化特性让这类计算变成一次底层的C语言级别循环。按类别分组的实现方式很多我当时用的是布尔索引。举个直观的例子想拿到所有Setosa样本只需构造一个布尔掩码labels 0然后用这个掩码去索引数组特征setosa_mask labels 0 setosa_features features[setosa_mask] print(setosa_features.shape) # (50, 4) print(setosa_features.mean(axis0)) # 每个特征的平均值axis0在这里指的是沿着行的方向取平均即对50条样本的同一列特征求均值返回一个有4个元素的数组。初次接触axis的同学经常搞反方向我的记忆口诀是axis0是“跨行”操作输出长度等于列数axis1是“跨列”操作输出长度等于行数。这个规则理解透了后面很多运算都不会迷糊。三类样本的均值一对比花瓣长度和花瓣宽度的差异非常明显花萼宽度则相对接近。具体数值不同版本的数据集可能略有出入但规律是一致的花瓣的两个特征在三个类别间的区分度远高于花萼特征。这个结论直接指导了后续特征筛选的方向。3.2 方差和标准差揭示了什么信息均值的局限性在于它只能告诉我们“中心在哪里”无法告诉我们“数据有多分散”。用Numpy算标准差一个std方法就够了overall_std features.std(axis0) class_std features[labels 0].std(axis0)花萼宽度这个特征有意思的地方在于它的整体标准差不小但按类别分组后每个组内部的标准差也没有显著变小。这说明花萼宽度在不同类别之间的重叠程度高用它来区分Versicolor和Virginica效果很差。反过来花瓣宽度在Setosa内部的方差极小几乎都集中在一个窄区间内这就让它成了判别力最强的特征之一。标准差还有一层实际工程含义在后续做归一化的时候Z-score标准化就是直接拿原始值减去均值再除以标准差。没有这一步标准化KNN算法中取值范围大的特征会“绑架”距离计算。这个话题下面细说但你现在应该已经能感觉到分析阶段算出的均值和方差不是白算的它们就是要被用在后面的预处理环节。3.3 手写相关系数从公式到代码相关性分析在这个项目里的作用是筛选特征。直观理解如果一个特征和类别标签关系密切那么不同类别在这个特征上的取值应该明显分开如果一个特征和类别关系弱那么用它做分类就像用身高去猜一个人喜欢吃什么菜基本靠蒙。我们没法直接用皮尔逊相关系数去算“特征与类别”的相关性因为类别是离散的0、1、2算出来的数值意义不大。但我可以退一步先算特征之间的相关性看看有没有冗余特征。比如花瓣长度和花瓣宽度如果它们的相关性极高说明这两个特征携带的信息大部分重叠建模时可以考虑只保留一个。手写皮尔逊相关系数公式并不复杂。给定两个特征向量x和y先各自减去均值再点乘后除以各自标准差的乘积def pearson_corr(x, y): x_centered x - x.mean() y_centered y - y.mean() numerator np.dot(x_centered, y_centered) denominator np.sqrt(np.dot(x_centered, x_centered) * np.dot(y_centered, y_centered)) return numerator / denominator corr_pl_pw pearson_corr(features[:, 2], features[:, 3]) print(f花瓣长度与花瓣宽度相关系数: {corr_pl_pw:.3f})这个代码用到了Numpy的两个基础操作广播减法x - x.mean()是将数组的每个元素都减去一个标量和点积np.dot。点积在这里的含义是“两个向量的逐元素乘积之和”它天然适合计算分子部分。实测花瓣长度和宽度之间的相关系数通常在0.96左右属于强相关。而这个强相关背后还有一层生物学解释花瓣的两个维度往往一起缩放这恰好印证了不同鸢尾花种类在花瓣形态上的分化程度。计算特征两两相关系数矩阵可以直接用np.corrcoef(features, rowvarFalse)返回一个4x4的对称矩阵对角线全是1。看到这个矩阵你会立刻明白哪些特征是“一伙的”。3.4 分类别统计分析的两个实用技巧均值、标准差是全体样本层面的统计但做分类分析时更常用的是“按类别分组后分别统计”。除了用布尔掩码还有一个冷门但好用的Numpy操作np.add.at配合计数器计算每个类别的特征总和再除以每类的样本数。sum_by_class np.zeros((3, 4)) count_by_class np.zeros(3) np.add.at(sum_by_class, labels, features) np.add.at(count_by_class, labels, 1) mean_by_class sum_by_class / count_by_class[:, None]这里count_by_class[:, None]的作用是把一维数组变成列向量形状从(3,)变为(3,1)这样除法的广播才能按行正确作用。这个技巧在真实场景中很有用因为np.add.at可以处理重复索引比直接sum_by_class[labels] features安全——后者在有重复索引时只会保留最后一次累加的结果是一个经典陷阱。另一个实用技巧是np.quantile计算分位数。均值容易被极端值拉偏分位数则能反映数据的分布形态。用np.quantile(features[:, 2], [0.25, 0.5, 0.75])能直接得到花瓣长度的四分位数一眼看出分布是否偏斜。数据分析不是只能看均值标准差多换几个视角往往有意外发现。4. 特征之间的“关系网”距离计算与Numpy广播机制4.1 为什么KNN算法必须做归一化KNN的核心思路很简单一个样本属于哪个类别看它最近的K个邻居属于哪个类别。而“最近”的定义最常用的就是欧氏距离。欧氏距离的公式相信大家都见过两个点对应坐标相减、平方、求和、再开方。问题来了鸢尾花数据集中花萼长度取值范围大概在4.3到7.9厘米花萼宽度在2.0到4.4厘米而花瓣长度在1.0到6.9厘米。这些特征数值范围各不相同如果不做任何处理直接用欧氏距离数值范围大的特征比如花瓣长度会在距离计算中占据绝对主导地位范围小的特征比如花萼宽度几乎被忽略。这就好比你要比较两个人的“综合体型”但只拿身高一个指标说话完全不考虑体重。解决方案就是归一化常见的有两种Min-Max标准化和Z-score标准化。Min-Max把所有特征压缩到[0, 1]区间公式是(x - min) / (max - min)Z-score把数据变成均值为0、标准差为1的分布公式是(x - mean) / std。对于KNN这种基于距离的算法两种都可行我更倾向于用Z-score因为它对离群值没那么敏感Min-Max遇到极端值会把正常数据压缩到非常窄的区间。def zscore_normalize(X): mean X.mean(axis0) std X.std(axis0) return (X - mean) / std这里要注意std不能为0——如果某个特征在所有样本上取值完全一样除以0会得到inf或者nan。真实数据中这种情况不常见但一旦遇到需要先检查特征是否有效。这个检查习惯一定要养成。4.2 手写欧氏距离三重循环、二重循环、全向量化归一化完成后来到整个项目最核心的部分计算任意两个样本之间的距离。最朴素的想法是三重循环外层遍历测试集中层遍历训练集内层遍历特征维度逐个累加平方差。def euclidean_distance_naive(X_train, X_test): m_test X_test.shape[0] m_train X_train.shape[0] dists np.zeros((m_test, m_train)) for i in range(m_test): for j in range(m_train): dist 0.0 for k in range(X_train.shape[1]): dist (X_test[i, k] - X_train[j, k]) ** 2 dists[i, j] np.sqrt(dist) return dists三重循环能跑通150条样本也不太慢但它完全没有体现Numpy的价值。先优化成二重循环用np.sum和np.sqrt替代内层循环。def euclidean_distance_sqrt(X_train, X_test): dists np.zeros((X_test.shape[0], X_train.shape[0])) for i in range(X_test.shape[0]): diff X_train - X_test[i] # 广播每一行都减去同一个测试样本 sq np.sum(diff ** 2, axis1) dists[i] np.sqrt(sq) return dists这一步的关键是广播机制X_train形状是(120, 4)X_test[i]形状是(4,)两者相减时Numpy会自动把(4,)扩展成(120, 4)相当于一次性完成了120次减法。axis1再对所有特征维度求和一下就拿到120个距离。这个写法比三重循环快得多而且代码更短。但还能更极致——完全去掉Python层的循环用矩阵运算一次算出所有距离。这需要用到欧氏距离的展开公式||a - b||² ||a||² ||b||² - 2·a·b对应到代码里就是def euclidean_distance_matrix(X_train, X_test): sum_train np.sum(X_train ** 2, axis1) # (m_train,) sum_test np.sum(X_test ** 2, axis1) # (m_test,) cross np.dot(X_test, X_train.T) # (m_test, m_train) dists np.sqrt(sum_test[:, None] sum_train[None, :] - 2 * cross) return distssum_test[:, None]把(30,)变成(30,1)sum_train[None, :]把(120,)变成(1,120)两者相加得到一个(30,120)的矩阵再用广播减去2倍交叉项。这个写法一次性得到全部距离矩阵没有显式循环速度是最快的。三种写法跑同样的小数据集三重循环和二重循环的耗时差异可能只有几毫秒因为数据量太小了。但矩阵化写法的优势在大数据集上会呈数量级拉开。更重要的是理解这种“用广播替代循环”的思维方式是Numpy进阶的核心门槛。4.3 广播机制的细节与常见坑广播机制虽然强大但它有个致命特点出错时不报错只是默默给你算出“错误的正确结果”。最常见的问题是维度对不上时Numpy做了隐式扩展导致结果形状完全不符合预期。举个例子sum_test[:, None] sum_train[None, :]得到的形状是(30, 120)如果你忘了加None直接sum_test sum_train两个一维数组(30,)和(120,)形状不一致Numpy不会报错但会试图广播——如果30和120不满足“其中一个为1或相等”的条件会直接抛ValueError如果碰巧满足条件又会算出形状完全不对的结果。所以每次做广播运算后养成print(result.shape)的习惯特别重要。另外要注意np.dot(X_test, X_train.T)中矩阵维度的方向。X_test是(30,4)X_train.T是(4,120)点积得到(30,120)每一行存储的是某个测试样本与所有训练样本的内积。内积越大说明两个向量方向越一致但距离要的是“越小越近”所以最后要用加和减2倍的方式展开。方向搞反了得到的距离矩阵就是转置的后续取最近邻居时全乱套。5. 手搓一个KNN分类器投票机制与性能对比5.1 训练集和测试集怎么切分KNN是一种“懒惰学习”算法它没有显式的训练过程所谓的“训练”其实就是把训练数据存起来预测时直接拿测试样本跟所有训练样本算距离。所以数据切分非常关键否则会出现严重的数据泄漏问题。我按70/30比例切分105条训练45条测试。为了不打乱类别分布使用分层抽样——保证训练集和测试集中每一类样本占比大致相同。Numpy没有现成的train_test_split但这个功能手写起来很容易先按类别索引分组每组内部随机选70%做训练其余做测试。np.random.seed(42) # 固定随机种子保证可复现 train_indices [] test_indices [] for cls in range(3): cls_indices np.where(labels cls)[0] np.random.shuffle(cls_indices) split_point int(len(cls_indices) * 0.7) train_indices.extend(cls_indices[:split_point]) test_indices.extend(cls_indices[split_point:]) train_features features[train_indices] train_labels labels[train_indices] test_features features[test_indices] test_labels labels[test_indices]这里有两个细节值得强调。一是np.random.seed(42)——固定随机种子否则每次运行切分结果不同模型评估不可复现你也没法判断改动代码后效果变好是因为算法改进还是数据组合变了。二是切分前先对索引shuffle如果直接按顺序切分鸢尾花数据集天然按类别排序存放前50条Setosa中间50条Versicolor后面50条Virginica顺序切分会直接导致训练集中缺少某些类别。我见过不少人在这踩坑忘记打乱训练集里全是前两类测试集里全是第三类模型准确率直接崩到30%左右还找不到原因。5.2 多数投票从argmax到bincount有了距离矩阵KNN的预测步骤是对每一个测试样本找出距离最小的K个训练样本统计这K个邻居的类别众数作为预测结果。取距离最小的K个索引可以用np.argsort排完序后取前K个即可。但如果要追求性能更推荐np.argpartition——它不保证完整排序只保证前K个是最小的速度比完整排序快不少K 5 top_k_indices np.argpartition(dists[i], K)[:K] top_k_labels train_labels[top_k_indices]拿到邻居标签后怎么投票最朴素的方法是循环计数。但Numpy提供了np.bincount专治这种整数频次统计votes np.bincount(top_k_labels, minlength3) predicted_label np.argmax(votes)minlength3保证输出数组长度至少为3——如果这K个邻居恰好全部是某一个类别其他类别的计数就是0也必须出现在数组里。argmax取频次最高的类别索引作为预测结果。需要留意的是np.bincount要求输入是非负整数数组train_labels恰好满足。如果你的标签是字符串或者浮点数就得分两步走先映射成整数索引再统计频次。这里也能看出前面把字符串标签映射成0/1/2这一步为后面省了多少事。5.3 用Numpy一次性预测所有测试样本写好针对单个测试样本的逻辑后可以再进一步向量化循环遍历测试集但每次循环内部不写任何Python循环。dists euclidean_distance_matrix(train_features, test_features) # (45, 105) predictions np.zeros(test_features.shape[0], dtypeint) for i in range(test_features.shape[0]): top_k_indices np.argpartition(dists[i], K)[:K] votes np.bincount(train_labels[top_k_indices], minlength3) predictions[i] np.argmax(votes)这个版本已经能跑但还不够“彻底向量化”。如果还想更快可以尝试把argpartition推广到整矩阵np.argpartition(dists, K, axis1)[:, :K]一步拿到所有测试样本的K近邻索引。好处是代码更短坏处是中间矩阵更大内存占用上升。对小数据集无所谓但对更大规模的数据你得在速度和内存之间做权衡。预测完成之后评估准确率accuracy np.mean(predictions test_labels) print(fK5 准确率: {accuracy:.3f})predictions test_labels得到布尔数组np.mean自动把True当作1、False当作0直接得到平均正确率。这个写法比手动sum除以len简洁得多也是Numpy风格的一种体现——能一行表达的计算就不要写三行。5.4 不同K值下的表现与边界问题K的选择直接影响分类效果。K1时模型对噪声极其敏感一个离群点就能让整个预测翻车K值太大又会让远距离的样本参与投票抹平类别的局部结构。实践中的做法是遍历K从1到15画出准确率变化曲线找一个稳定区间。用Numpy实现K值扫描很容易外层套一个循环内部复用距离矩阵。因为距离计算只需要做一次无论K取多少都不用重新算。这是KNN的一个天然优势——训练阶段零成本预测阶段的主要开销全在距离计算上。一个容易忽略的点是平票问题。当K是偶数且某两类票数相同时argmax默认返回索引较小的类别这会引入系统性偏差。Spark等大数据框架里平票的解决方案五花八门但鸢尾花数据集上K5和K7通常都不会遇到平票问题。如果确实遇到了可以改成随机打破平局或者把K往奇数调。5.5 三种距离实现方案的真实时间对比我在本地实际跑过一次三种距离实现的耗时对比。数据集就用鸢尾花的150条样本测试集30条三次运行取均值实现方式耗时毫秒直观感受三重循环约1.32肉眼能感觉到“卡顿”二重循环广播约0.21基本无感全矩阵化约0.08工业级标准差距看起来不大毕竟数据量只有150条。但把样本量换成15000条三重循环直接跑不动矩阵化版本可能也内存吃紧这时需要分块计算或者用更专业的工具。Numpy的向量化能力在于它把计算密集的循环下沉到C语言层面执行Python层的循环次数越少性能越好。6. 真实踩坑记录与Numpy版本差异6.1 忘记打乱数据导致的“假高准确率”我在复现一个开源代码时遇到过一件事对方没有打乱数据但准确率却有97%。原因是他切分时恰好把测试集里的Setosa全部留在了测试集而这个类别的特征太好区分了即使后两类分错很多整体准确率依然被拉高。这就是数据泄漏的一种形式——信息从训练集“流向”了测试集评估结果虚高。更常见的情况是打乱后忘记固定随机种子导致每次运行准确率波动。比如总准确率在0.90到0.97之间跳来跳去你根本说不清K5比K3好是因为算法真实差异还是随机运气。解决办法很粗暴任何时候涉及随机操作第一行写np.random.seed(一个固定整数)。这个习惯不只对KNN有效对任何需要复现的算法实验都适用。6.2 广播维度错误当你以为“差不多”时Numpy广播的报错信息通常长这样ValueError: operands could not be broadcast together with shapes (45,) (105,)。初学者看到这个很容易懵因为两个数组确实可以“相加”啊——比如[1,2,3] [4,5,6]是等长相加但不等长时Numpy并不自动帮你做“对齐”而是尝试能不能补齐维度。45和105不满足“其中一个维度是1或相等”的条件于是直接报错。我在做距离矩阵时第一次也踩了这个坑减掉均值那一步X - X.mean(axis0)没问题因为X.mean(axis0)返回形状(4,)广播规则可以自动匹配最后一维。但到了sum_test[:, None] sum_train[None, :]这一步如果我忘了加None就会触发上面的错误。解决方法是每次广播前画一个形状草图左边标出维度右边标出要广播的维度手动验证是否满足规则。等熟能生巧之后可以省掉这一步但初期不要偷懒。6.3 argpartition返回的索引是乱序的np.argpartition的返回值有个容易坑人的特性它不保证前K个元素按大小顺序排列只保证它们都在前K个位置内。比如我想取最小的5个距离对应的索引argpartition(dists, 5)[:5]拿到的可能是[3, 1, 9, 2, 7]乱序的。这个特点对我的KNN投票逻辑没有影响因为投票只关心集合不关心顺序。但如果你后续需要按距离从近到远展示邻居必须对前K个索引再做一次np.argsort否则输出顺序是乱的。我因为没注意这个细节一度怀疑自己的代码有bug——排查了半天才发现是argpartition本身的特性不是算法逻辑错。6.4 不同Numpy版本的行为差异Numpy在1.20版本之前np.bincount对非整数输入的处理相对宽松偶尔能带过float数组不报错。但从某些版本开始输入必须是整数数组否则直接抛TypeError。我自己就在一次升级环境后遇到了这个问题——之前能跑的旧代码突然报错查了半天文档才明白是版本行为变化。另外np.random.shuffle和np.random.permutation有细微差别前者原地打乱不返回新数组后者返回打乱后的副本不改原数组。混淆这两个API会导致你打了半天乱原数组还纹丝不动。这在旧版本Numpy里也一样属于设计上就容易被人忽略的点。我的建议是写完代码后把当前Numpy版本记下来np.__version__或者直接固定在某个长期支持版本上。机器学习工程里环境一致性是个大话题Numpy作为底层库版本一升上面所有依赖的行为都可能跟着变。6.5 手写评估函数的两个细节准确率是最粗粒度的评估指标但它在类别不均衡时很容易骗人。比如测试集里80%是某个类别模型全猜这个类别也能有80%准确率。鸢尾花数据本身均衡可你得为未来的项目做准备——更稳的做法是看混淆矩阵即每个真实类别和预测类别的交叉计数。用Numpy手写一个2x2混淆矩阵很简单二分类时多分类时用循环逐类统计def confusion_matrix(y_true, y_pred, num_classes): matrix np.zeros((num_classes, num_classes), dtypeint) for t, p in zip(y_true, y_pred): matrix[t, p] 1 return matrix这个函数很直接但在Numpy风格上还能再优化让所有(真实标签, 预测标签)对一次性进到矩阵累加位置。用np.add.at也可以原理跟前面按类别求和一样。能看到这里的话你应该对np.add.at很熟悉了——它就是专门处理带重复索引的累加场景的。7. 分析结论与方向延伸建议整个项目跑完你会得到几个非常确定的结论第一Setosa这个类别和其他两类在花瓣特征上的分布几乎不重叠任意特征组合都能把它完美区分出来第二Versicolor和Virginica在花萼特征上有较高的重叠区域想要区分它们必须依赖花瓣特征而且即使只用花瓣长度和花瓣宽度依然存在少量交界处样本第三KNN在K5左右能达到大约95%的准确率说明特征空间中的类别边界并不复杂但仍有少数样本位于两个类别边界附近。基于这些结论有几条延伸方向可以作为后续练手。一是把鸢尾花数据集换成真实业务数据比如二手车的价格预测回归问题那时你需要把KNN的多数投票改成均值回归距离计算保持不变。二是试试在Numpy基础上引入Scikit-learn做对照实验看看手写的KNN和官方实现准确率是否一致——这是个很有效的验证手段能帮你发现隐藏的bug。三是扩展到高维数据比如手写数字识别每张图片28x28像素展平成784维特征KNN会立刻暴露出维数灾难问题距离度量几乎失效这也反向解释了为什么深度学习在图像领域大放异彩。从教学角度来看Numpy版鸢尾花分析最大的价值在于它把机器学习里“数据准备-特征理解-算法实现-模型评估”的完整链路压缩到一个小而美的项目里每个环节你都亲手接触过后面再学Pandas、Scikit-learn时你会很清楚它们在替你做什么、为什么要那样设计。这是我个人实操中最真实的体会——高级工具永远在帮我们省时间但只有亲手实现过一遍核心逻辑你对工具的理解才不是空中楼阁。把这套代码保存好过几个月数据量大了再回来看你会感谢当初这个基础打得扎实。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →